@@ -2,7 +2,7 @@ use std::{collections::HashSet, ops::Deref};
22
33use base64:: { Engine as _, engine:: general_purpose:: STANDARD as BASE64_STANDARD } ;
44use callisto:: {
5- bot_installations, bot_keys, bot_tokens, bots,
5+ bot_installations, bot_keys, bot_tokens, bots, git_repo ,
66 sea_orm_active_enums:: {
77 BotStatusEnum , InstallationBotStatusEnum , InstallationTargetTypeEnum , PermissionScopeEnum ,
88 } ,
@@ -32,6 +32,8 @@ use crate::{
3232const BOT_TOKEN_PREFIX : & str = "bot_" ;
3333const BOT_TOKEN_RANDOM_LEN : usize = 32 ;
3434const BOT_TOKEN_HMAC_KEY_ENV : & str = "MEGA_BOT_TOKEN_HMAC_SECRET" ;
35+ /// Organization installation target_id for system bots (`organization_id` is null).
36+ const SYSTEM_ORG_INSTALL_TARGET_ID : i64 = 0 ;
3537
3638#[ derive( Clone ) ]
3739pub struct BotsStorage {
@@ -281,8 +283,10 @@ impl BotsStorage {
281283
282284 /// Ensure the fixed mega-init bot exists and return a fresh push token.
283285 ///
284- /// Creates bot `mega-init` if missing, revokes any existing `mega-init-push`
285- /// tokens, then issues a new token (plaintext returned once).
286+ /// Creates bot `mega-init` if missing, ensures a system Organization
287+ /// installation (so receive-pack accepts its Write token), revokes any
288+ /// existing `mega-init-push` tokens, then issues a new token (plaintext
289+ /// returned once).
286290 pub async fn ensure_init_bot_token ( & self ) -> Result < ( bots:: Model , String ) , MegaError > {
287291 const INIT_BOT_NAME : & str = "mega-init" ;
288292 const INIT_TOKEN_NAME : & str = "mega-init-push" ;
@@ -303,6 +307,8 @@ impl BotsStorage {
303307 ) ) ) ;
304308 }
305309
310+ self . ensure_system_org_installation ( bot. id ) . await ?;
311+
306312 self . revoke_bot_tokens_by_name ( bot. id , INIT_TOKEN_NAME )
307313 . await ?;
308314 let ( _model, token_plain) = self
@@ -311,6 +317,110 @@ impl BotsStorage {
311317 Ok ( ( bot, token_plain) )
312318 }
313319
320+ /// Ensure an Enabled Organization installation for a system bot (target_id 0).
321+ pub async fn ensure_system_org_installation ( & self , bot_id : i64 ) -> Result < ( ) , MegaError > {
322+ let existing = bot_installations:: Entity :: find ( )
323+ . filter ( bot_installations:: Column :: BotId . eq ( bot_id) )
324+ . filter (
325+ bot_installations:: Column :: TargetType . eq ( InstallationTargetTypeEnum :: Organization ) ,
326+ )
327+ . filter ( bot_installations:: Column :: TargetId . eq ( SYSTEM_ORG_INSTALL_TARGET_ID ) )
328+ . one ( self . get_connection ( ) )
329+ . await ?;
330+
331+ if let Some ( inst) = existing {
332+ if inst. status != InstallationBotStatusEnum :: Enabled {
333+ let mut active: bot_installations:: ActiveModel = inst. into_active_model ( ) ;
334+ active. status = Set ( InstallationBotStatusEnum :: Enabled ) ;
335+ active. update ( self . get_connection ( ) ) . await ?;
336+ }
337+ return Ok ( ( ) ) ;
338+ }
339+
340+ self . install_bot (
341+ bot_id,
342+ InstallationTargetTypeEnum :: Organization ,
343+ SYSTEM_ORG_INSTALL_TARGET_ID ,
344+ 0 ,
345+ )
346+ . await ?;
347+ Ok ( ( ) )
348+ }
349+
350+ /// Whether `permission_scope` is sufficient for git receive-pack (push).
351+ pub fn scope_allows_push ( scope : & PermissionScopeEnum ) -> bool {
352+ matches ! (
353+ scope,
354+ PermissionScopeEnum :: Write | PermissionScopeEnum :: Admin
355+ )
356+ }
357+
358+ /// Resolve an import `git_repo.id` for `repo_path`, if registered.
359+ pub async fn find_repo_id_by_path ( & self , repo_path : & str ) -> Result < Option < i64 > , MegaError > {
360+ let path = repo_path. trim_end_matches ( '/' ) ;
361+ let path = if path. is_empty ( ) { "/" } else { path } ;
362+
363+ if let Some ( repo) = git_repo:: Entity :: find ( )
364+ . filter ( git_repo:: Column :: RepoPath . eq ( path) )
365+ . one ( self . get_connection ( ) )
366+ . await ?
367+ {
368+ return Ok ( Some ( repo. id ) ) ;
369+ }
370+
371+ // Longest registered prefix (same idea as find_git_repo_like_path).
372+ let like = git_repo:: Entity :: find ( )
373+ . filter ( Expr :: cust ( format ! ( "'{path}' LIKE repo_path || '%'" ) ) )
374+ . order_by_desc ( Expr :: cust ( "LENGTH(repo_path)" ) )
375+ . one ( self . get_connection ( ) )
376+ . await ?;
377+ Ok ( like. map ( |r| r. id ) )
378+ }
379+
380+ /// True if the bot may authenticate a receive-pack push to `repo_path`.
381+ ///
382+ /// Requires Write/Admin scope and an Enabled installation covering the
383+ /// target: Repository install for the resolved git_repo, and/or an
384+ /// Organization install for the bot's org (or system target_id 0).
385+ pub async fn bot_may_push_to_path (
386+ & self ,
387+ bot : & bots:: Model ,
388+ repo_path : & str ,
389+ ) -> Result < bool , MegaError > {
390+ if bot. status != BotStatusEnum :: Enabled {
391+ return Ok ( false ) ;
392+ }
393+ if !Self :: scope_allows_push ( & bot. permission_scope ) {
394+ return Ok ( false ) ;
395+ }
396+
397+ let installations = self . get_installed_bot_by_id ( bot. id ) . await ?;
398+ let enabled: Vec < _ > = installations
399+ . into_iter ( )
400+ . filter ( |i| i. status == InstallationBotStatusEnum :: Enabled )
401+ . collect ( ) ;
402+ if enabled. is_empty ( ) {
403+ return Ok ( false ) ;
404+ }
405+
406+ let org_target_id = bot. organization_id . unwrap_or ( SYSTEM_ORG_INSTALL_TARGET_ID ) ;
407+ let has_org_install = enabled. iter ( ) . any ( |i| {
408+ i. target_type == InstallationTargetTypeEnum :: Organization
409+ && i. target_id == org_target_id
410+ } ) ;
411+
412+ let repo_id = self . find_repo_id_by_path ( repo_path) . await ?;
413+ if let Some ( repo_id) = repo_id {
414+ let has_repo_install = enabled. iter ( ) . any ( |i| {
415+ i. target_type == InstallationTargetTypeEnum :: Repository && i. target_id == repo_id
416+ } ) ;
417+ return Ok ( has_repo_install || has_org_install) ;
418+ }
419+
420+ // Monorepo / unregistered path: Organization install only.
421+ Ok ( has_org_install)
422+ }
423+
314424 /// Generate a new bot token, persist its HMAC-SHA256 hash and return the model with plaintext.
315425 pub async fn generate_bot_token (
316426 & self ,
0 commit comments