fix(iam): align OIDC parent IDs with MinIO (#5290)

* fix(iam): align OIDC parent IDs with MinIO

* test(iam): cover OIDC STS binding policy lookup

* test(admin): use runtime facade for AppContext setup

* test(ilm): wait for lifecycle backfill before manual failure

* test(ilm): make overlapping admission check concurrent
This commit is contained in:
GatewayJ
2026-07-27 15:16:22 +08:00
committed by GitHub
parent e076e8cc6e
commit 24cf2cdb78
7 changed files with 259 additions and 65 deletions
+7 -31
View File
@@ -18,7 +18,6 @@ use serde_json::Value;
use std::collections::HashMap;
pub const OIDC_VIRTUAL_PARENT_CLAIM: &str = "x-rustfs-internal-oidc-parent";
const OIDC_VIRTUAL_PARENT_PREFIX: &str = "openid=";
#[derive(Debug, Clone)]
pub struct FederatedClaims {
@@ -65,18 +64,13 @@ impl FederatedAuthorization {
return None;
}
let subject_len = u64::try_from(subject.len()).ok()?;
let issuer_len = u64::try_from(issuer.len()).ok()?;
let mut source = Vec::with_capacity(16 + subject.len() + issuer.len());
source.extend_from_slice(&subject_len.to_be_bytes());
let mut source = Vec::with_capacity(8 + subject.len() + issuer.len());
source.extend_from_slice(b"openid:");
source.extend_from_slice(subject.as_bytes());
source.extend_from_slice(&issuer_len.to_be_bytes());
source.push(b':');
source.extend_from_slice(issuer.as_bytes());
let digest = HashAlgorithm::SHA256.hash_encode(&source);
Some(format!(
"{OIDC_VIRTUAL_PARENT_PREFIX}{}",
base64_simd::URL_SAFE_NO_PAD.encode_to_string(digest.as_ref())
))
Some(base64_simd::URL_SAFE_NO_PAD.encode_to_string(digest.as_ref()))
}
}
@@ -151,7 +145,7 @@ mod tests {
}
#[test]
fn oidc_virtual_parent_is_stable_and_issuer_scoped() {
fn oidc_virtual_parent_matches_minio_and_is_issuer_scoped() {
let first = authorization(Vec::new(), Vec::new());
let mut second = first.clone();
second
@@ -161,9 +155,9 @@ mod tests {
assert_eq!(
first.oidc_virtual_parent().as_deref(),
Some("openid=pUmguI1petsjVfDFQppmmR9yqdmWnBAXGJhHV_s9W3I")
Some("TwyekekG2eMes0qk9Tgh7KXEitwGi1z2W1f2KccrXGA")
);
assert!(rustfs_policy::auth::contains_reserved_chars(
assert!(!rustfs_policy::auth::contains_reserved_chars(
first.oidc_virtual_parent().as_deref().expect("virtual parent")
));
assert_ne!(first.oidc_virtual_parent(), second.oidc_virtual_parent());
@@ -188,22 +182,4 @@ mod tests {
assert_ne!(plain.oidc_virtual_parent(), padded.oidc_virtual_parent());
}
#[test]
fn oidc_virtual_parent_encoding_is_unambiguous() {
let mut first = authorization(Vec::new(), Vec::new());
first.claims.sub = "subject".to_string();
first.claims.raw.insert(
"iss".to_string(),
Value::String("https://issuer.example/path:https://other.example".to_string()),
);
let mut second = authorization(Vec::new(), Vec::new());
second.claims.sub = "subject:https://issuer.example/path".to_string();
second
.claims
.raw
.insert("iss".to_string(), Value::String("https://other.example".to_string()));
assert_ne!(first.oidc_virtual_parent(), second.oidc_virtual_parent());
}
}
+62 -1
View File
@@ -907,7 +907,41 @@ where
return Err(Error::InvalidArgument);
}
let (mut policies, _) = self.policy_db_get_internal(name, false, false).await?;
let (policies, _) = self.policy_db_get_internal(name, false, false).await?;
self.policy_db_get_with_groups(policies, groups).await
}
pub async fn sts_policy_db_get(&self, name: &str, groups: &Option<Vec<String>>) -> Result<Vec<String>> {
if name.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let sts_policies = Arc::clone(&cache.sts_policies);
drop(cache);
let mapped_policy = match sts_policies.get(name) {
Some(policy) => policy.clone(),
None => {
let mut policies = HashMap::new();
if let Err(err) = self.api.load_mapped_policy(name, UserType::Sts, false, &mut policies).await
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
match policies.get(name) {
Some(policy) => {
self.cache.add_or_update_sts_policy(name, policy, OffsetDateTime::now_utc());
policy.clone()
}
None => MappedPolicy::default(),
}
}
};
self.policy_db_get_with_groups(mapped_policy.to_slice(), groups).await
}
async fn policy_db_get_with_groups(&self, mut policies: Vec<String>, groups: &Option<Vec<String>>) -> Result<Vec<String>> {
let present = !policies.is_empty();
if let Some(groups) = groups {
@@ -2585,6 +2619,33 @@ mod tests {
}
}
#[tokio::test]
async fn sts_policy_lookup_ignores_colliding_regular_user_mapping() {
let iam = build_test_iam_cache(FailingInitialLoadStore);
let parent = "TwyekekG2eMes0qk9Tgh7KXEitwGi1z2W1f2KccrXGA";
let user = UserIdentity::new(Credentials {
access_key: parent.to_string(),
secret_key: "regular-user-secret".to_string(),
status: STATUS_ENABLED.to_string(),
..Default::default()
});
let now = OffsetDateTime::now_utc();
iam.cache.add_or_update_user(parent, &user, now);
iam.cache
.add_or_update_user_policy(parent, &MappedPolicy::new("regular-policy"), now);
iam.cache
.add_or_update_sts_policy(parent, &MappedPolicy::new("oidc-policy"), now);
assert_eq!(
iam.policy_db_get(parent, &None).await.expect("regular lookup should succeed"),
vec!["regular-policy"]
);
assert_eq!(
iam.sts_policy_db_get(parent, &None).await.expect("STS lookup should succeed"),
vec!["oidc-policy"]
);
}
#[tokio::test]
async fn set_temp_user_retries_until_sts_identity_becomes_visible() {
let store = DelayedTempUserVisibilityStore::new(2);
+17 -1
View File
@@ -872,6 +872,10 @@ impl<T: Store> IamSys<T> {
self.store.policy_db_get(name, groups).await
}
pub async fn sts_policy_db_get(&self, name: &str, groups: &Option<Vec<String>>) -> Result<Vec<String>> {
self.store.sts_policy_db_get(name, groups).await
}
/// Check whether a policy name from a JWT claim is safe to resolve against the IAM store.
///
/// Allowed characters: `[a-zA-Z0-9_:.-]`
@@ -2776,7 +2780,7 @@ mod tests {
async fn oidc_service_account_uses_verified_policy_and_persisted_boundary() {
ensure_test_global_credentials();
let iam_sys = IamSys::new(IamCache::new(StsTestMockStore::new(true)).await.unwrap());
let parent_user = "openid=pUmguI1petsjVfDFQppmmR9yqdmWnBAXGJhHV_s9W3I";
let parent_user = "TwyekekG2eMes0qk9Tgh7KXEitwGi1z2W1f2KccrXGA";
let mut oidc_claims = HashMap::from([
("iss".to_string(), Value::String("rustfs-oidc".to_string())),
("oidc_provider".to_string(), Value::String("default".to_string())),
@@ -3466,6 +3470,18 @@ mod tests {
);
}
#[tokio::test]
async fn test_sts_policy_lookup_loads_missing_mapping_without_regular_user() {
let store = StsTestMockStore::new(false);
let cache_manager = IamCache::new(store).await.unwrap();
let iam_sys = IamSys::new(cache_manager);
let policies = iam_sys.sts_policy_db_get("notify-sts-parent", &None).await.unwrap();
assert_eq!(policies, vec!["readwrite"]);
assert!(iam_sys.store.cache.snapshot().sts_policies.contains_key("notify-sts-parent"));
}
#[tokio::test]
async fn test_missing_user_notification_cleans_related_cache_state() {
let store = StsTestMockStore::new(false);