1- //! Path-aware admin permission operations.
1+ //! Global admin permission operations.
22//!
3- //! This module provides admin permission checking with path context support .
4- //! Each root directory (e.g., project, doc, release) can have its own admin list
5- //! defined in its `.mega_cedar.json` file .
3+ //! This module provides admin permission checking for the monorepo system .
4+ //! All admin permissions are defined in a single `.mega_cedar.json` file
5+ //! located in the root directory (`/`) .
66//!
77//! # Design
8- //! - Admin permissions are scoped to root directories (first-level dirs under /)
9- //! - Each root directory has its own `.mega_cedar.json` file
10- //! - Cache keys are namespaced by both instance prefix and root directory
11- //! - Empty paths fall back to the default directory (project)
8+ //! - A single global admin list applies to the entire monorepo
9+ //! - The admin configuration file is stored at `/.mega_cedar.json`
10+ //! - Redis caching is used to avoid repeated file parsing
1211
1312use common:: errors:: MegaError ;
1413use git_internal:: internal:: object:: tree:: Tree ;
@@ -17,123 +16,82 @@ use redis::AsyncCommands;
1716
1817use crate :: api_service:: mono_api_service:: MonoApiService ;
1918
20- /// Cache TTL for admin lists (10 minutes).
19+ /// Cache TTL for admin list (10 minutes).
2120pub const ADMIN_CACHE_TTL : u64 = 600 ;
2221
23- /// The Cedar entity file name in each root directory.
22+ /// The Cedar entity file name in root directory.
2423pub const ADMIN_FILE : & str = ".mega_cedar.json" ;
2524
26- /// Default root directory when path is empty or root.
27- pub const DEFAULT_ROOT_DIR : & str = "project" ;
28-
29- /// Extract the root directory from a path.
30- ///
31- /// Examples:
32- /// - `/project/src/main.rs` -> `project`
33- /// - `/doc/readme.md` -> `doc`
34- /// - `/` or empty -> `project` (default)
35- pub fn extract_root_dir ( path : & str ) -> String {
36- let path = path. trim_start_matches ( '/' ) ;
37- path. split ( '/' )
38- . next ( )
39- . filter ( |s| !s. is_empty ( ) )
40- . unwrap_or ( DEFAULT_ROOT_DIR )
41- . to_string ( )
42- }
25+ /// Redis cache key suffix for admin list.
26+ const ADMIN_CACHE_KEY_SUFFIX : & str = "admin:list" ;
4327
4428impl MonoApiService {
45- /// Check if a user is an admin for the specified path context.
46- pub async fn check_is_admin ( & self , username : & str , path : & str ) -> Result < bool , MegaError > {
47- let root_dir = extract_root_dir ( path) ;
48- let admins = self . get_effective_admins ( & root_dir) . await ?;
29+ /// Check if a user is an admin.
30+ pub async fn check_is_admin ( & self , username : & str ) -> Result < bool , MegaError > {
31+ let admins = self . get_effective_admins ( ) . await ?;
4932 Ok ( admins. contains ( & username. to_string ( ) ) )
5033 }
5134
52- /// Retrieve all admin usernames for the specified path context.
53- pub async fn get_all_admins ( & self , path : & str ) -> Result < Vec < String > , MegaError > {
54- let root_dir = extract_root_dir ( path) ;
55- self . get_effective_admins ( & root_dir) . await
35+ /// Retrieve all admin usernames.
36+ pub async fn get_all_admins ( & self ) -> Result < Vec < String > , MegaError > {
37+ self . get_effective_admins ( ) . await
5638 }
5739
5840 /// Get admins from cache or storage.
59- async fn get_effective_admins ( & self , root_dir : & str ) -> Result < Vec < String > , MegaError > {
60- if let Ok ( admins) = self . get_admins_from_cache ( root_dir) . await {
41+ /// This method first attempts to read from Redis cache. On cache miss,
42+ /// it loads the admin list from the `.mega_cedar.json` file and caches
43+ /// the result.
44+ async fn get_effective_admins ( & self ) -> Result < Vec < String > , MegaError > {
45+ if let Ok ( admins) = self . get_admins_from_cache ( ) . await {
6146 return Ok ( admins) ;
6247 }
6348
64- let store = self . load_admin_entity_store ( root_dir ) . await ?;
49+ let store = self . load_admin_entity_store ( ) . await ?;
6550 let resolver = saturn:: admin_resolver:: AdminResolver :: from_entity_store ( & store) ;
6651 let admins = resolver. admin_list ( ) ;
6752
68- if let Err ( e) = self . cache_admins ( root_dir , & admins) . await {
69- tracing:: warn!( "Failed to write admin cache for {} : {}" , root_dir , e) ;
53+ if let Err ( e) = self . cache_admins ( & admins) . await {
54+ tracing:: warn!( "Failed to write admin cache: {}" , e) ;
7055 }
7156
7257 Ok ( admins)
7358 }
7459
75- /// Invalidate the admin list cache for a root directory.
76- pub async fn invalidate_admin_cache ( & self , root_dir : & str ) {
60+ /// Invalidate the admin list cache.
61+ /// This should be called when the `.mega_cedar.json` file is modified.
62+ pub async fn invalidate_admin_cache ( & self ) {
7763 let mut conn = self . git_object_cache . connection . clone ( ) ;
78- let key = format ! ( "{}:admin:list:{}" , self . git_object_cache. prefix, root_dir) ;
64+ let key = format ! (
65+ "{}:{}" ,
66+ self . git_object_cache. prefix, ADMIN_CACHE_KEY_SUFFIX
67+ ) ;
7968 if let Err ( e) = conn. del :: < _ , ( ) > ( & key) . await {
80- tracing:: warn!( "Failed to invalidate admin cache for {} : {}" , root_dir , e) ;
69+ tracing:: warn!( "Failed to invalidate admin cache: {}" , e) ;
8170 }
8271 }
8372
84- /// Load EntityStore from `/{root_dir}/.mega_cedar.json`.
85- async fn load_admin_entity_store (
86- & self ,
87- root_dir : & str ,
88- ) -> Result < saturn:: entitystore:: EntityStore , MegaError > {
89- let dir_path = format ! ( "/{}" , root_dir) ;
73+ /// Load EntityStore from `/.mega_cedar.json`.
74+ async fn load_admin_entity_store ( & self ) -> Result < saturn:: entitystore:: EntityStore , MegaError > {
9075 let mono_storage = self . storage . mono_storage ( ) ;
9176
92- let target_tree = if let Ok ( Some ( dir_ref) ) = mono_storage. get_main_ref ( & dir_path) . await {
93- Tree :: from_mega_model (
94- mono_storage
95- . get_tree_by_hash ( & dir_ref. ref_tree_hash )
96- . await ?
97- . ok_or_else ( || {
98- MegaError :: Other ( format ! ( "Tree {} not found" , dir_ref. ref_tree_hash) )
99- } ) ?,
100- )
101- } else {
102- // Fallback: traverse from root to find the directory
103- let root_ref = mono_storage
104- . get_main_ref ( "/" )
77+ let root_ref = mono_storage
78+ . get_main_ref ( "/" )
79+ . await ?
80+ . ok_or_else ( || MegaError :: Other ( "Root ref not found" . into ( ) ) ) ?;
81+
82+ let root_tree = Tree :: from_mega_model (
83+ mono_storage
84+ . get_tree_by_hash ( & root_ref. ref_tree_hash )
10585 . await ?
106- . ok_or_else ( || MegaError :: Other ( "Root ref not found" . into ( ) ) ) ?;
107-
108- let root_tree = Tree :: from_mega_model (
109- mono_storage
110- . get_tree_by_hash ( & root_ref. ref_tree_hash )
111- . await ?
112- . ok_or_else ( || MegaError :: Other ( "Root tree not found" . into ( ) ) ) ?,
113- ) ;
114-
115- let dir_item = root_tree
116- . tree_items
117- . iter ( )
118- . find ( |item| item. name == root_dir)
119- . ok_or_else ( || MegaError :: Other ( format ! ( "'{}' directory not found" , root_dir) ) ) ?;
120-
121- Tree :: from_mega_model (
122- mono_storage
123- . get_tree_by_hash ( & dir_item. id . to_string ( ) )
124- . await ?
125- . ok_or_else ( || {
126- MegaError :: Other ( format ! ( "Tree for '{}' not found" , root_dir) )
127- } ) ?,
128- )
129- } ;
130-
131- let blob_item = target_tree
86+ . ok_or_else ( || MegaError :: Other ( "Root tree not found" . into ( ) ) ) ?,
87+ ) ;
88+
89+ let blob_item = root_tree
13290 . tree_items
13391 . iter ( )
13492 . find ( |item| item. name == ADMIN_FILE )
13593 . ok_or_else ( || {
136- MegaError :: Other ( format ! ( "{} not found in /{} " , ADMIN_FILE , root_dir ) )
94+ MegaError :: Other ( format ! ( "{} not found in root directory " , ADMIN_FILE ) )
13795 } ) ?;
13896
13997 let content_bytes = self
@@ -149,9 +107,12 @@ impl MonoApiService {
149107 . map_err ( |e| MegaError :: Other ( format ! ( "JSON parse failed: {}" , e) ) )
150108 }
151109
152- async fn get_admins_from_cache ( & self , root_dir : & str ) -> Result < Vec < String > , MegaError > {
110+ async fn get_admins_from_cache ( & self ) -> Result < Vec < String > , MegaError > {
153111 let mut conn = self . git_object_cache . connection . clone ( ) ;
154- let key = format ! ( "{}:admin:list:{}" , self . git_object_cache. prefix, root_dir) ;
112+ let key = format ! (
113+ "{}:{}" ,
114+ self . git_object_cache. prefix, ADMIN_CACHE_KEY_SUFFIX
115+ ) ;
155116 let data: Option < String > = conn. get ( & key) . await ?;
156117
157118 match data {
@@ -161,12 +122,15 @@ impl MonoApiService {
161122 }
162123 }
163124
164- async fn cache_admins ( & self , root_dir : & str , admins : & [ String ] ) -> Result < ( ) , MegaError > {
125+ async fn cache_admins ( & self , admins : & [ String ] ) -> Result < ( ) , MegaError > {
165126 let mut conn = self . git_object_cache . connection . clone ( ) ;
166127 let json = serde_json:: to_string ( admins)
167128 . map_err ( |e| MegaError :: Other ( format ! ( "Serialize failed: {}" , e) ) ) ?;
168129
169- let key = format ! ( "{}:admin:list:{}" , self . git_object_cache. prefix, root_dir) ;
130+ let key = format ! (
131+ "{}:{}" ,
132+ self . git_object_cache. prefix, ADMIN_CACHE_KEY_SUFFIX
133+ ) ;
170134 conn. set_ex :: < _ , _ , ( ) > ( & key, json, ADMIN_CACHE_TTL ) . await ?;
171135 Ok ( ( ) )
172136 }
0 commit comments