From f255b8a9f1caf5b61d3ad57c796b0b188833b60a Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Fri, 17 Apr 2026 13:38:18 +0800 Subject: [PATCH] fix(admin): align accountinfo policy with IAM prepare_auth for OIDC console (#2568) Co-authored-by: GatewayJ <8352692332qq.com> Co-authored-by: houseme --- crates/iam/src/sys.rs | 38 ++++++++++++++ rustfs/src/admin/handlers/account_info.rs | 63 +++++++---------------- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/crates/iam/src/sys.rs b/crates/iam/src/sys.rs index 699e39c17..bd5604f8f 100644 --- a/crates/iam/src/sys.rs +++ b/crates/iam/src/sys.rs @@ -134,6 +134,19 @@ impl PreparedIamAuth { } } } + + /// Returns the resolved identity policy prepared for the current auth mode. + /// + /// This is intended for read-only views (for example `/accountinfo`) so + /// callers can reuse the same policy resolution path as authorization. + pub fn combined_policy_for_view(&self) -> Option<&Policy> { + match &self.mode { + PreparedIamMode::Regular { combined_policy } => Some(combined_policy), + PreparedIamMode::Sts { combined_policy, .. } => Some(combined_policy), + PreparedIamMode::ServiceAccount { combined_policy, .. } => Some(combined_policy), + PreparedIamMode::Opa | PreparedIamMode::Owner | PreparedIamMode::Deny => None, + } + } } impl IamSys { @@ -1286,6 +1299,31 @@ mod tests { use std::collections::HashMap; use time::OffsetDateTime; + #[test] + fn test_combined_policy_for_view_returns_regular_policy() { + let policy = Policy { + version: "2012-10-17".to_string(), + ..Default::default() + }; + let prepared = PreparedIamAuth { + needs_existing_object_tag: false, + mode: PreparedIamMode::Regular { combined_policy: policy }, + }; + + let resolved = prepared.combined_policy_for_view(); + assert_eq!(resolved.map(|p| p.version.as_str()), Some("2012-10-17")); + } + + #[test] + fn test_combined_policy_for_view_returns_none_for_deny() { + let prepared = PreparedIamAuth { + needs_existing_object_tag: false, + mode: PreparedIamMode::Deny, + }; + + assert!(prepared.combined_policy_for_view().is_none()); + } + /// Mock Store for STS tests: either group-attached policies via parent user, or no IAM policies. #[derive(Clone)] struct StsTestMockStore { diff --git a/rustfs/src/admin/handlers/account_info.rs b/rustfs/src/admin/handlers/account_info.rs index b9f8a320e..99096c475 100644 --- a/rustfs/src/admin/handlers/account_info.rs +++ b/rustfs/src/admin/handlers/account_info.rs @@ -23,7 +23,6 @@ use rustfs_credentials::get_global_action_cred; use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys; use rustfs_ecstore::new_object_layer_fn; use rustfs_ecstore::store_api::{BucketOperations, BucketOptions, StorageAPI}; -use rustfs_iam::store::MappedPolicy; use rustfs_policy::policy::BucketPolicy; use rustfs_policy::policy::default::DEFAULT_POLICIES; use rustfs_policy::policy::{Args, action::Action, action::S3Action}; @@ -146,22 +145,6 @@ impl Operation for AccountInfoHandler { cred.access_key.clone() }; - let claims_args = Args { - account: "", - groups: &None, - action: Action::None, - bucket: "", - conditions: &HashMap::new(), - is_owner: false, - object: "", - claims, - deny_only: false, - }; - - let role_arn = claims_args.get_role_arn(); - - // TODO: get_policies_from_claims(claims); - let Some(admin_cred) = get_global_action_cred() else { return Err(S3Error::with_message( S3ErrorCode::InternalError, @@ -178,35 +161,25 @@ impl Operation for AccountInfoHandler { break; } } - } else if let Some(arn) = role_arn { - let (_, policy_name) = iam_store - .get_role_policy(arn) - .await - .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?; - - let policies = MappedPolicy::new(&policy_name).to_slice(); - effective_policy = iam_store.get_combined_policy(&policies).await; - } else if let Some(claim_policies) = claims.get("policy").and_then(|v| v.as_str()) { - // STS/OIDC users: resolve policy names from JWT claims against built-in policies - let mut resolved = Vec::new(); - for policy_name in claim_policies.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { - for (name, p) in DEFAULT_POLICIES.iter() { - if *name == policy_name { - resolved.push(p.clone()); - break; - } - } - } - if !resolved.is_empty() { - effective_policy = rustfs_policy::policy::Policy::merge_policies(resolved); - } } else { - let policies = iam_store - .policy_db_get(&account_name, &cred.groups) - .await - .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get policy failed: {e}")))?; - - effective_policy = iam_store.get_combined_policy(&policies).await; + // Reuse the canonical IAM preparation path so accountinfo policy view + // stays in sync with real authorization semantics (STS/group fallback included). + let empty_conditions = HashMap::new(); + let auth_args = Args { + account: &cred.access_key, + groups: &cred.groups, + action: Action::None, + bucket: "", + conditions: &empty_conditions, + is_owner: owner, + object: "", + claims, + deny_only: false, + }; + let prepared = iam_store.prepare_auth(&auth_args).await; + if let Some(policy) = prepared.combined_policy_for_view() { + effective_policy = policy.clone(); + } }; let policy_str = serde_json::to_string(&effective_policy)