From 87ceb518e82f14cc0757967cacd17f9f67b6a932 Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Wed, 18 Mar 2026 21:22:58 +0800 Subject: [PATCH 1/7] #1999: rebase Signed-off-by: Hongze Gao <15101764808@163.com> --- jupiter/src/storage/bots_storage.rs | 30 +++++++++++++++++++++++++++++ mono/src/api/router/mod.rs | 1 + 2 files changed, 31 insertions(+) diff --git a/jupiter/src/storage/bots_storage.rs b/jupiter/src/storage/bots_storage.rs index a94c20b0d..fa6341afc 100644 --- a/jupiter/src/storage/bots_storage.rs +++ b/jupiter/src/storage/bots_storage.rs @@ -395,3 +395,33 @@ fn compute_bot_token_hash(token_body: &str, key: &[u8]) -> String { mac.update(token_body.as_bytes()); hex::encode(mac.finalize().into_bytes()) } + +type HmacSha256 = Hmac; + +fn generate_bot_token_plain() -> Result { + use ring::rand::{SecureRandom, SystemRandom}; + + let rng = SystemRandom::new(); + let mut bytes = [0u8; BOT_TOKEN_RANDOM_LEN]; + rng.fill(&mut bytes).map_err(|_| { + MegaError::Other("failed to generate secure random bytes for bot token".to_string()) + })?; + + let encoded = BASE64_STANDARD.encode(bytes); + Ok(format!("{BOT_TOKEN_PREFIX}{encoded}")) +} + +fn load_bot_token_hmac_key() -> Result, MegaError> { + let secret = std::env::var(BOT_TOKEN_HMAC_KEY_ENV).map_err(|_| { + MegaError::Other(format!( + "{BOT_TOKEN_HMAC_KEY_ENV} is not set for bot token HMAC" + )) + })?; + Ok(secret.into_bytes()) +} + +fn compute_bot_token_hash(token_body: &str, key: &[u8]) -> String { + let mut mac = HmacSha256::new_from_slice(key).expect("HMAC-SHA256 can take a key of any size"); + mac.update(token_body.as_bytes()); + hex::encode(mac.finalize().into_bytes()) +} diff --git a/mono/src/api/router/mod.rs b/mono/src/api/router/mod.rs index 5db0a49a0..7795e6f02 100644 --- a/mono/src/api/router/mod.rs +++ b/mono/src/api/router/mod.rs @@ -1,5 +1,6 @@ pub mod admin_router; pub mod bot_router; +pub mod bot_router; pub mod buck_router; pub mod build_trigger_router; pub mod cl_router; From c20c9992320e457044c69d52572e74e1e6f001cb Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Sun, 15 Mar 2026 13:53:16 +0800 Subject: [PATCH 2/7] - Export bot_token_dto in jupiter model so BotTokenInfo is reachable - BotsStorage: add get_bot_by_id() for bot existence checks - BotsStorage: use update_many() in revoke_bot_tokens_by_bot to avoid N+1 - BotsStorage: validate HMAC secret (non-empty, min 32 chars) in load_bot_token_hmac_key - BotsStorage: use DateTimeWithTimeZone for expiry comparison in find_bot_by_token - AuditStorage: set created_at with DateTimeWithTimeZone (Utc::now().into()) - bot_router: ensure bot exists before create/list/revoke; return 404 when missing - bot_router: validate expires_in range (1..=10y) to avoid overflow/panic - tests: add audit_storage to AppService in test_storage() Signed-off-by: Hongze Gao <15101764808@163.com> --- jupiter/src/storage/bots_storage.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/jupiter/src/storage/bots_storage.rs b/jupiter/src/storage/bots_storage.rs index fa6341afc..dfd041e15 100644 --- a/jupiter/src/storage/bots_storage.rs +++ b/jupiter/src/storage/bots_storage.rs @@ -417,7 +417,19 @@ fn load_bot_token_hmac_key() -> Result, MegaError> { "{BOT_TOKEN_HMAC_KEY_ENV} is not set for bot token HMAC" )) })?; - Ok(secret.into_bytes()) + let trimmed = secret.trim(); + if trimmed.is_empty() { + return Err(MegaError::Other(format!( + "{BOT_TOKEN_HMAC_KEY_ENV} must not be empty for bot token HMAC" + ))); + } + if trimmed.len() < BOT_TOKEN_HMAC_MIN_LEN { + return Err(MegaError::Other(format!( + "{BOT_TOKEN_HMAC_KEY_ENV} is too short for bot token HMAC; it must be at least {} characters long", + BOT_TOKEN_HMAC_MIN_LEN + ))); + } + Ok(trimmed.as_bytes().to_vec()) } fn compute_bot_token_hash(token_body: &str, key: &[u8]) -> String { From 974ed6f1587d4742e081e179041d063b78541fbe Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Wed, 18 Mar 2026 21:09:41 +0800 Subject: [PATCH 3/7] #1999: fix identity extraction in cedar_guard middleware and revoke_bot_token 404 OpenAPI docs - Use req.extract() to resolve LoginUser/BotIdentity instead of direct extensions access - Fix missing Bot/Human context and incorrect "reader" fallback - Update 404 description to "Bot not found" - Align docs with idempotent implementation Signed-off-by: Hongze Gao <15101764808@163.com> --- mono/src/api/guard/cedar_guard.rs | 8 ++++---- mono/src/api/router/bot_router.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mono/src/api/guard/cedar_guard.rs b/mono/src/api/guard/cedar_guard.rs index 10e2a3023..ab3f4f83a 100644 --- a/mono/src/api/guard/cedar_guard.rs +++ b/mono/src/api/guard/cedar_guard.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, path::Path, str::FromStr}; use axum::{ - extract::{FromRef, Request, State}, + extract::{FromRef, Request, RequestPartsExt, State}, middleware::Next, response::Response, }; @@ -109,7 +109,7 @@ fn match_operation( pub async fn cedar_guard( State(state): State, - req: Request, + mut req: Request, next: Next, ) -> Result { let request_path = req.uri().path().to_owned(); @@ -138,8 +138,8 @@ pub async fn cedar_guard( // .ok_or_else(|| MegaError::with_message(format!("Change list not found for link: {}", link)))?; // let repo_path: PathBuf = cl_model.path.into(); - let login_user = req.extensions().get::(); - let bot_identity = req.extensions().get::(); + let bot_identity = req.extract::().await.ok(); + let login_user = req.extract::().await.ok(); let (principal_type, principal_id) = if let Some(bot) = bot_identity { ("Bot".to_string(), bot.bot.id.to_string()) diff --git a/mono/src/api/router/bot_router.rs b/mono/src/api/router/bot_router.rs index 7fd3cb023..d05f765d7 100644 --- a/mono/src/api/router/bot_router.rs +++ b/mono/src/api/router/bot_router.rs @@ -314,7 +314,7 @@ async fn list_bot_tokens( (status = 200, description = "Token revoked successfully"), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden - admin only"), - (status = 404, description = "Bot or token not found"), + (status = 404, description = "Bot not found"), ), tag = BOT_TAG )] From ccb6e638e17e37195ce243d08f64e1bde785ee10 Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Fri, 20 Mar 2026 20:57:05 +0800 Subject: [PATCH 4/7] #1999: remove duplicate bot token helpers and module export - remove duplicated in - deduplicate bot token HMAC helpers in to fix E0428 - short-circuit auth extraction in : try first, then only if needed Signed-off-by: Hongze Gao <15101764808@163.com> --- jupiter/src/storage/bots_storage.rs | 42 ----------------------------- mono/src/api/guard/cedar_guard.rs | 3 +-- mono/src/api/router/mod.rs | 1 - 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/jupiter/src/storage/bots_storage.rs b/jupiter/src/storage/bots_storage.rs index dfd041e15..a94c20b0d 100644 --- a/jupiter/src/storage/bots_storage.rs +++ b/jupiter/src/storage/bots_storage.rs @@ -395,45 +395,3 @@ fn compute_bot_token_hash(token_body: &str, key: &[u8]) -> String { mac.update(token_body.as_bytes()); hex::encode(mac.finalize().into_bytes()) } - -type HmacSha256 = Hmac; - -fn generate_bot_token_plain() -> Result { - use ring::rand::{SecureRandom, SystemRandom}; - - let rng = SystemRandom::new(); - let mut bytes = [0u8; BOT_TOKEN_RANDOM_LEN]; - rng.fill(&mut bytes).map_err(|_| { - MegaError::Other("failed to generate secure random bytes for bot token".to_string()) - })?; - - let encoded = BASE64_STANDARD.encode(bytes); - Ok(format!("{BOT_TOKEN_PREFIX}{encoded}")) -} - -fn load_bot_token_hmac_key() -> Result, MegaError> { - let secret = std::env::var(BOT_TOKEN_HMAC_KEY_ENV).map_err(|_| { - MegaError::Other(format!( - "{BOT_TOKEN_HMAC_KEY_ENV} is not set for bot token HMAC" - )) - })?; - let trimmed = secret.trim(); - if trimmed.is_empty() { - return Err(MegaError::Other(format!( - "{BOT_TOKEN_HMAC_KEY_ENV} must not be empty for bot token HMAC" - ))); - } - if trimmed.len() < BOT_TOKEN_HMAC_MIN_LEN { - return Err(MegaError::Other(format!( - "{BOT_TOKEN_HMAC_KEY_ENV} is too short for bot token HMAC; it must be at least {} characters long", - BOT_TOKEN_HMAC_MIN_LEN - ))); - } - Ok(trimmed.as_bytes().to_vec()) -} - -fn compute_bot_token_hash(token_body: &str, key: &[u8]) -> String { - let mut mac = HmacSha256::new_from_slice(key).expect("HMAC-SHA256 can take a key of any size"); - mac.update(token_body.as_bytes()); - hex::encode(mac.finalize().into_bytes()) -} diff --git a/mono/src/api/guard/cedar_guard.rs b/mono/src/api/guard/cedar_guard.rs index ab3f4f83a..842dcc957 100644 --- a/mono/src/api/guard/cedar_guard.rs +++ b/mono/src/api/guard/cedar_guard.rs @@ -139,11 +139,10 @@ pub async fn cedar_guard( // let repo_path: PathBuf = cl_model.path.into(); let bot_identity = req.extract::().await.ok(); - let login_user = req.extract::().await.ok(); let (principal_type, principal_id) = if let Some(bot) = bot_identity { ("Bot".to_string(), bot.bot.id.to_string()) - } else if let Some(user) = login_user { + } else if let Some(user) = req.extract::().await.ok() { ("User".to_string(), user.username.clone()) } else { ("User".to_string(), "reader".to_string()) diff --git a/mono/src/api/router/mod.rs b/mono/src/api/router/mod.rs index 7795e6f02..5db0a49a0 100644 --- a/mono/src/api/router/mod.rs +++ b/mono/src/api/router/mod.rs @@ -1,6 +1,5 @@ pub mod admin_router; pub mod bot_router; -pub mod bot_router; pub mod buck_router; pub mod build_trigger_router; pub mod cl_router; From e435a3faf680cb88cf31bd43bdd9a5773098db14 Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Fri, 20 Mar 2026 22:25:37 +0800 Subject: [PATCH 5/7] #1999: use stateful axum extractors in cedar guard middleware - replace state-less request extraction in with for both and - fix incorrect import by removing and importing instead - keep short-circuit principal resolution: try bot identity first, then attempt login user only when bot extraction fails Signed-off-by: Hongze Gao <15101764808@163.com> --- mono/src/api/guard/cedar_guard.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mono/src/api/guard/cedar_guard.rs b/mono/src/api/guard/cedar_guard.rs index 842dcc957..685fc60d7 100644 --- a/mono/src/api/guard/cedar_guard.rs +++ b/mono/src/api/guard/cedar_guard.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, path::Path, str::FromStr}; use axum::{ - extract::{FromRef, Request, RequestPartsExt, State}, + extract::{FromRef, FromRequestParts, Request, State}, middleware::Next, response::Response, }; @@ -138,11 +138,16 @@ pub async fn cedar_guard( // .ok_or_else(|| MegaError::with_message(format!("Change list not found for link: {}", link)))?; // let repo_path: PathBuf = cl_model.path.into(); - let bot_identity = req.extract::().await.ok(); + let bot_identity = BotIdentity::from_request_parts(req.parts_mut(), &state) + .await + .ok(); let (principal_type, principal_id) = if let Some(bot) = bot_identity { ("Bot".to_string(), bot.bot.id.to_string()) - } else if let Some(user) = req.extract::().await.ok() { + } else if let Some(user) = LoginUser::from_request_parts(req.parts_mut(), &state) + .await + .ok() + { ("User".to_string(), user.username.clone()) } else { ("User".to_string(), "reader".to_string()) From cd5927e1af9df124508d9c69a310eb19cc60b81a Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Sat, 21 Mar 2026 13:23:35 +0800 Subject: [PATCH 6/7] #1999: use request parts API for stateful identity extraction in cedar_guard - replace invalid usage with and extract identities from - run first, then fall back to - preserve middleware flow by reconstructing the request with before calling Signed-off-by: Hongze Gao <15101764808@163.com> --- mono/src/api/guard/cedar_guard.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mono/src/api/guard/cedar_guard.rs b/mono/src/api/guard/cedar_guard.rs index 685fc60d7..450439fe4 100644 --- a/mono/src/api/guard/cedar_guard.rs +++ b/mono/src/api/guard/cedar_guard.rs @@ -109,7 +109,7 @@ fn match_operation( pub async fn cedar_guard( State(state): State, - mut req: Request, + req: Request, next: Next, ) -> Result { let request_path = req.uri().path().to_owned(); @@ -138,16 +138,15 @@ pub async fn cedar_guard( // .ok_or_else(|| MegaError::with_message(format!("Change list not found for link: {}", link)))?; // let repo_path: PathBuf = cl_model.path.into(); - let bot_identity = BotIdentity::from_request_parts(req.parts_mut(), &state) + let (mut parts, body) = req.into_parts(); + + let bot_identity = BotIdentity::from_request_parts(&mut parts, &state) .await .ok(); let (principal_type, principal_id) = if let Some(bot) = bot_identity { ("Bot".to_string(), bot.bot.id.to_string()) - } else if let Some(user) = LoginUser::from_request_parts(req.parts_mut(), &state) - .await - .ok() - { + } else if let Some(user) = LoginUser::from_request_parts(&mut parts, &state).await.ok() { ("User".to_string(), user.username.clone()) } else { ("User".to_string(), "reader".to_string()) @@ -173,6 +172,8 @@ pub async fn cedar_guard( MegaError::Other(format!("Guard Authorization failed: {}", e)), ) })?; + + let req = Request::from_parts(parts, body); let response = next.run(req).await; if response.status().is_client_error() { From 9167ce21199f58aa41196f7c0a244026d6ddb710 Mon Sep 17 00:00:00 2001 From: Hongze Gao <15101764808@163.com> Date: Sat, 21 Mar 2026 13:43:33 +0800 Subject: [PATCH 7/7] #1999: satisfy clippy by matching extractor results directly - replace + with direct matching in - keep principal resolution order unchanged: first, then , then fallback Signed-off-by: Hongze Gao <15101764808@163.com> --- mono/src/api/guard/cedar_guard.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/mono/src/api/guard/cedar_guard.rs b/mono/src/api/guard/cedar_guard.rs index 450439fe4..91af0e368 100644 --- a/mono/src/api/guard/cedar_guard.rs +++ b/mono/src/api/guard/cedar_guard.rs @@ -140,17 +140,14 @@ pub async fn cedar_guard( let (mut parts, body) = req.into_parts(); - let bot_identity = BotIdentity::from_request_parts(&mut parts, &state) - .await - .ok(); - - let (principal_type, principal_id) = if let Some(bot) = bot_identity { - ("Bot".to_string(), bot.bot.id.to_string()) - } else if let Some(user) = LoginUser::from_request_parts(&mut parts, &state).await.ok() { - ("User".to_string(), user.username.clone()) - } else { - ("User".to_string(), "reader".to_string()) - }; + let (principal_type, principal_id) = + if let Ok(bot) = BotIdentity::from_request_parts(&mut parts, &state).await { + ("Bot".to_string(), bot.bot.id.to_string()) + } else if let Ok(user) = LoginUser::from_request_parts(&mut parts, &state).await { + ("User".to_string(), user.username.clone()) + } else { + ("User".to_string(), "reader".to_string()) + }; // let policy_path = repo_path.join("cedar/policies.cedar"); // let policy_content = get_blob_string(&state, &policy_path).await?;