test(iam): freeze OIDC federation boundaries (#8393)

This commit is contained in:
GatewayJ
2026-10-07 20:05:10 +08:00
committed by GitHub
parent db2abcf0a6
commit 19978b2cb6
4 changed files with 206 additions and 45 deletions
+91 -27
View File
@@ -11386,6 +11386,36 @@ mod tests {
}
}
fn ldap_settings_kvs() -> rustfs_config::server_config::KVS {
rustfs_config::server_config::KVS(vec![
rustfs_config::server_config::KV {
key: "enable".to_string(),
value: "on".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "user_dn_search_base_dn".to_string(),
value: "ou=people,dc=example,dc=com".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "user_dn_search_filter".to_string(),
value: "(uid=%s)".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "group_search_base_dn".to_string(),
value: "ou=groups,dc=example,dc=com".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "group_search_filter".to_string(),
value: "(&(objectclass=groupOfNames)(member=%s))".to_string(),
hidden_if_empty: false,
},
])
}
// rustfs#7003: the site region is per-site and must not reach the peer
// IDP comparison while OpenID is unconfigured, or two sites in different
// regions can never be paired even with an identical (empty) IDP config.
@@ -11412,6 +11442,66 @@ mod tests {
assert_eq!(settings.claim_provider.client_id, "client-a");
}
#[test]
fn idp_settings_preserve_site_replication_wire_shape() {
let (ldap, ldap_configs) = ldap_settings_from_kvs(&ldap_settings_kvs());
let settings = IDPSettings {
ldap,
ldap_configs,
open_id: open_id_settings(
vec![
("default".to_string(), openid_provider("client-a")),
("corp".to_string(), openid_provider("client-b")),
],
"eu-site-1".to_string(),
),
};
assert_eq!(
serde_json::to_value(settings).expect("serialize IDP settings"),
serde_json::json!({
"LDAP": {
"IsLDAPEnabled": true,
"LDAPUserDNSearchBase": "ou=people,dc=example,dc=com",
"LDAPUserDNSearchFilter": "(uid=%s)",
"LDAPGroupSearchBase": "ou=groups,dc=example,dc=com",
"LDAPGroupSearchFilter": "(&(objectclass=groupOfNames)(member=%s))",
},
"LDAPConfigs": {
"Enabled": true,
"Configs": {
"default": {
"UserDNSearchBase": "ou=people,dc=example,dc=com",
"UserDNSearchFilter": "(uid=%s)",
"GroupSearchBase": "ou=groups,dc=example,dc=com",
"GroupSearchFilter": "(&(objectclass=groupOfNames)(member=%s))",
}
}
},
"OpenID": {
"Enabled": true,
"Region": "eu-site-1",
"Roles": {
"corp": {
"ClaimName": "groups",
"ClaimUserinfoEnabled": false,
"RolePolicy": "readwrite",
"ClientID": "client-b",
"HashedClientSecret": "hashed",
}
},
"ClaimProvider": {
"ClaimName": "groups",
"ClaimUserinfoEnabled": false,
"RolePolicy": "readwrite",
"ClientID": "client-a",
"HashedClientSecret": "hashed",
}
}
})
);
}
// A whole provider object present on one side only must not spill its
// credential-derived leaves into the rejection message.
#[test]
@@ -14494,33 +14584,7 @@ mod tests {
#[test]
fn test_ldap_settings_from_kvs_reads_minio_style_keys() {
let kvs = rustfs_config::server_config::KVS(vec![
rustfs_config::server_config::KV {
key: "enable".to_string(),
value: "on".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "user_dn_search_base_dn".to_string(),
value: "ou=people,dc=example,dc=com".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "user_dn_search_filter".to_string(),
value: "(uid=%s)".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "group_search_base_dn".to_string(),
value: "ou=groups,dc=example,dc=com".to_string(),
hidden_if_empty: false,
},
rustfs_config::server_config::KV {
key: "group_search_filter".to_string(),
value: "(&(objectclass=groupOfNames)(member=%s))".to_string(),
hidden_if_empty: false,
},
]);
let kvs = ldap_settings_kvs();
let (ldap, ldap_configs) = ldap_settings_from_kvs(&kvs);
+43 -13
View File
@@ -434,15 +434,23 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu
let new_cred = &session.credentials;
let subject = authorization.claims.session_identity();
// Build XML response (AssumeRoleWithWebIdentityResponse)
let expiration = new_cred
let xml = assume_role_with_web_identity_response_xml(new_cred, &subject);
let mut resp = S3Response::new((StatusCode::OK, Body::from(xml.into_bytes())));
resp.headers
.insert(http::header::CONTENT_TYPE, HeaderValue::from_static("application/xml"));
Ok(resp)
}
fn assume_role_with_web_identity_response_xml(credentials: &rustfs_credentials::Credentials, subject: &str) -> String {
let expiration = credentials
.expiration
.unwrap_or_else(|| OffsetDateTime::now_utc().saturating_add(Duration::seconds(3600)));
let exp_str = expiration
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
let xml = format!(
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithWebIdentityResult>
@@ -455,17 +463,12 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu
<SubjectFromWebIdentityToken>{}</SubjectFromWebIdentityToken>
</AssumeRoleWithWebIdentityResult>
</AssumeRoleWithWebIdentityResponse>"#,
xml_escape(&new_cred.access_key),
xml_escape(&new_cred.secret_key),
xml_escape(&new_cred.session_token),
xml_escape(&credentials.access_key),
xml_escape(&credentials.secret_key),
xml_escape(&credentials.session_token),
xml_escape(&exp_str),
xml_escape(&subject),
);
let mut resp = S3Response::new((StatusCode::OK, Body::from(xml.into_bytes())));
resp.headers
.insert(http::header::CONTENT_TYPE, HeaderValue::from_static("application/xml"));
Ok(resp)
xml_escape(subject),
)
}
/// Escape special XML characters in a string.
@@ -561,6 +564,33 @@ mod tests {
assert_eq!(xml_escape("it's"), "it&apos;s");
}
#[test]
fn web_identity_response_xml_preserves_wire_shape() {
let credentials = rustfs_credentials::Credentials {
access_key: "access<key".to_string(),
secret_key: "secret&key".to_string(),
session_token: "session\"token".to_string(),
expiration: Some(OffsetDateTime::UNIX_EPOCH),
..Default::default()
};
assert_eq!(
assume_role_with_web_identity_response_xml(&credentials, "subject'name"),
r#"<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithWebIdentityResult>
<Credentials>
<AccessKeyId>access&lt;key</AccessKeyId>
<SecretAccessKey>secret&amp;key</SecretAccessKey>
<SessionToken>session&quot;token</SessionToken>
<Expiration>1970-01-01T00:00:00Z</Expiration>
</Credentials>
<SubjectFromWebIdentityToken>subject&apos;name</SubjectFromWebIdentityToken>
</AssumeRoleWithWebIdentityResult>
</AssumeRoleWithWebIdentityResponse>"#
);
}
#[test]
fn test_duration_clamping() {
// Simulates the clamping logic from handle_assume_role_with_web_identity
+25 -5
View File
@@ -15,7 +15,7 @@
use rustfs_iam::{
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
get_oidc, init_oidc_sys_with_extra_root_ca_provider,
oidc::{OidcExtraRootCaMaterial, OidcExtraRootCaProvider},
oidc::{OidcExtraRootCaMaterial, OidcExtraRootCaProvider, OidcSys},
};
use std::{
collections::hash_map::DefaultHasher,
@@ -56,10 +56,7 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
match init_oidc_sys_with_extra_root_ca_provider(oidc_extra_root_ca_provider()).await {
Ok(()) => {
if let Some(oidc) = get_oidc() {
let adapter = Arc::new(StandardOidcAdapter::new(oidc));
let registry = FederatedIdentityRegistry::new(adapter);
let service = Arc::new(FederatedIdentityService::new(registry));
crate::runtime_sources::publish_federated_identity_service(service);
crate::runtime_sources::publish_federated_identity_service(standard_oidc_service(oidc));
}
}
Err(e) => {
@@ -76,6 +73,12 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
Ok(())
}
fn standard_oidc_service(oidc: Arc<OidcSys>) -> Arc<FederatedIdentityService> {
let adapter = Arc::new(StandardOidcAdapter::new(oidc));
let registry = FederatedIdentityRegistry::new(adapter);
Arc::new(FederatedIdentityService::new(registry))
}
pub(crate) fn oidc_extra_root_ca_provider() -> OidcExtraRootCaProvider {
OidcExtraRootCaProvider::new(current_oidc_extra_root_ca_material)
}
@@ -112,3 +115,20 @@ fn oidc_extra_root_ca_generation(outbound_generation: u64, root_ca_pem: Option<&
root_ca_pem.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_oidc_service_retains_one_oidc_arc() {
let oidc = Arc::new(OidcSys::empty().expect("empty OIDC configuration should be valid"));
let retained = Arc::clone(&oidc);
let service = standard_oidc_service(oidc);
assert_eq!(Arc::strong_count(&retained), 2, "the service should retain one OIDC Arc");
drop(service);
assert_eq!(Arc::strong_count(&retained), 1, "dropping the service should release its OIDC Arc");
}
}