diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml index 49ad6203b..7fb4f6b9f 100644 --- a/crates/iam/Cargo.toml +++ b/crates/iam/Cargo.toml @@ -30,6 +30,7 @@ workspace = true [features] default = [] +test-util = [] hotpath = [ "hotpath/hotpath", "hotpath/tokio", diff --git a/crates/iam/src/federation/mod.rs b/crates/iam/src/federation/mod.rs index 246b10de5..dc1d930f5 100644 --- a/crates/iam/src/federation/mod.rs +++ b/crates/iam/src/federation/mod.rs @@ -18,7 +18,6 @@ mod mapper; mod model; pub mod oidc; mod provider; -mod registry; mod transaction; pub use binding::FederatedSessionBinding; @@ -27,10 +26,9 @@ pub use mapper::{CoreFederatedAuthorizationMapper, FederatedAuthorizationRules}; pub(crate) use mapper::{FederatedAuthorizationRule, FederatedAuthorizationRuleRef}; pub(crate) use model::OpaqueLogoutContinuation; pub use model::{ - FederatedAuthorization, FederatedClaims, FederatedCodeExchange, FederatedLoginSession, FederatedProviderRef, - FederatedSession, FederatedSessionTransaction, OIDC_VIRTUAL_PARENT_CLAIM, VerifiedFederatedIdentity, + FederatedAuthorization, FederatedClaims, FederatedLoginSession, FederatedProviderRef, FederatedSession, + FederatedSessionTransaction, OIDC_VIRTUAL_PARENT_CLAIM, VerifiedFederatedIdentity, }; -pub use provider::{FederatedIdentityProvider, FederatedProviderView, FederatedRedirectPolicy}; -pub(crate) use provider::{StandardOidcAuthentication, VerifiedFederatedCodeExchange}; -pub use registry::FederatedIdentityRegistry; +pub(crate) use provider::{FederatedProviderQuery, StandardOidcAuthentication, VerifiedFederatedCodeExchange}; +pub use provider::{FederatedProviderView, FederatedRedirectPolicy}; pub use transaction::FederatedIdentityService; diff --git a/crates/iam/src/federation/model.rs b/crates/iam/src/federation/model.rs index acaef1c16..76b8a9816 100644 --- a/crates/iam/src/federation/model.rs +++ b/crates/iam/src/federation/model.rs @@ -164,40 +164,6 @@ impl FederatedAuthorization { } } -pub struct FederatedCodeExchange { - pub authorization: FederatedAuthorization, - pub redirect_after: Option, - pub id_token: String, -} - -impl FederatedCodeExchange { - pub(crate) fn into_parts(self) -> (FederatedAuthorization, Option, OpaqueLogoutContinuation) { - let Self { - authorization, - redirect_after, - id_token, - } = self; - let continuation = OpaqueLogoutContinuation { - provider: FederatedProviderRef::new(authorization.provider_id.clone()), - id_token, - }; - (authorization, redirect_after, continuation) - } -} - -impl fmt::Debug for FederatedCodeExchange { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("FederatedCodeExchange") - .field("provider_id", &self.authorization.provider_id) - .field("policy_count", &self.authorization.policies.len()) - .field("group_count", &self.authorization.groups.len()) - .field("redirect_after_present", &self.redirect_after.is_some()) - .field("id_token_present", &!self.id_token.is_empty()) - .finish() - } -} - /// Carries the provider-bound ID token between code exchange and logout token creation. pub(crate) struct OpaqueLogoutContinuation { provider: FederatedProviderRef, @@ -397,27 +363,13 @@ mod tests { #[test] fn logout_continuation_binds_provider_and_redacts_token() { - let mut authorization = authorization(Vec::new(), Vec::new()); - authorization - .claims - .raw - .insert("attribute_poison".to_string(), Value::String("secret-attribute".to_string())); - let exchange = FederatedCodeExchange { - authorization, - redirect_after: Some("/console?poison=redirect-secret".to_string()), - id_token: "secret-id-token".to_string(), - }; - let debug = format!("{exchange:?}"); - let (_, _, continuation) = exchange.into_parts(); + let continuation = + OpaqueLogoutContinuation::new(FederatedProviderRef::new("standard_oidc".to_string()), "secret-id-token".to_string()); let continuation_debug = format!("{continuation:?}"); let (provider, id_token) = continuation.into_parts(); assert_eq!(provider.as_str(), "standard_oidc"); assert_eq!(id_token, "secret-id-token"); - assert!(!debug.contains("secret-id-token")); - assert!(!debug.contains("secret-attribute")); - assert!(!debug.contains("redirect-secret")); - assert!(debug.contains("id_token_present: true")); assert!(!continuation_debug.contains("secret-id-token")); assert!(continuation_debug.contains("[REDACTED]")); } diff --git a/crates/iam/src/federation/oidc/flow.rs b/crates/iam/src/federation/oidc/flow.rs index 1e2e49d44..e338590e7 100644 --- a/crates/iam/src/federation/oidc/flow.rs +++ b/crates/iam/src/federation/oidc/flow.rs @@ -16,10 +16,9 @@ use super::config::OidcConfigQuery; use super::{claims, config, discovery, http}; use crate::{ federation::{ - CoreFederatedAuthorizationMapper, FederatedAuthorization, FederatedAuthorizationRules, FederatedCodeExchange, - FederatedIdentityProvider, FederatedIdentityService, FederatedProviderRef, FederatedProviderView, - FederatedRedirectPolicy, FederationError, OpaqueLogoutContinuation, Result, StandardOidcAuthentication, - VerifiedFederatedCodeExchange, + CoreFederatedAuthorizationMapper, FederatedAuthorizationRules, FederatedIdentityService, FederatedProviderQuery, + FederatedProviderRef, FederatedProviderView, FederatedRedirectPolicy, FederationError, OpaqueLogoutContinuation, Result, + StandardOidcAuthentication, VerifiedFederatedCodeExchange, }, oidc::{OidcConfigSnapshot, OidcSys}, }; @@ -45,8 +44,8 @@ impl StandardOidcAdapter { pub fn into_service(self: Arc, mapper: CoreFederatedAuthorizationMapper) -> FederatedIdentityService { let authentication: Arc = self.clone(); - let provider: Arc = self; - FederatedIdentityService::from_standard_oidc_parts(provider, authentication, mapper) + let provider_query: Arc = self; + FederatedIdentityService::from_standard_oidc_parts(provider_query, authentication, mapper) } } @@ -58,6 +57,10 @@ impl OidcConfigQuery for StandardOidcAdapter { #[async_trait::async_trait] impl StandardOidcAuthentication for StandardOidcAdapter { + async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option) -> Result { + http::authorize_url(&self.oidc, provider_id, redirect_uri, redirect_after).await + } + async fn exchange_identity(&self, state: &str, code: &str, redirect_uri: &str) -> Result { let (oidc_claims, provider_id, session, id_token) = http::exchange_code(&self.oidc, state, code, redirect_uri).await?; let provider = FederatedProviderRef::new(provider_id); @@ -81,10 +84,13 @@ impl StandardOidcAuthentication for StandardOidcAdapter { let (provider, id_token) = continuation.into_parts(); http::create_logout_token(&self.oidc, provider.as_str(), &id_token).await } + + async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result> { + http::build_logout_url(&self.oidc, logout_token, post_logout_redirect_uri).await + } } -#[async_trait::async_trait] -impl FederatedIdentityProvider for StandardOidcAdapter { +impl FederatedProviderQuery for StandardOidcAdapter { fn has_providers(&self) -> bool { self.oidc.has_providers() } @@ -97,40 +103,7 @@ impl FederatedIdentityProvider for StandardOidcAdapter { discovery::list_visible_providers(&self.oidc) } - fn provider_config(&self, provider_id: &str) -> Option<&crate::oidc::OidcProviderConfig> { - self.oidc.get_provider_config(provider_id) - } - fn redirect_policy(&self, provider_id: &str) -> Option { config::redirect_policy(&self.oidc, provider_id) } - - async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option) -> Result { - http::authorize_url(&self.oidc, provider_id, redirect_uri, redirect_after).await - } - - async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result { - let exchange = StandardOidcAuthentication::exchange_identity(self, state, code, redirect_uri).await?; - let (identity, redirect_after, continuation) = exchange.into_parts(); - let authorization = CoreFederatedAuthorizationMapper::new(self.authorization_rules()).map(identity); - let (_, id_token) = continuation.into_parts(); - Ok(FederatedCodeExchange { - authorization, - redirect_after, - id_token, - }) - } - - async fn verify_web_identity_token(&self, jwt: &str) -> Result { - let identity = StandardOidcAuthentication::verify_identity(self, jwt).await?; - Ok(CoreFederatedAuthorizationMapper::new(self.authorization_rules()).map(identity)) - } - - async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result { - http::create_logout_token(&self.oidc, provider_id, id_token).await - } - - async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result> { - http::build_logout_url(&self.oidc, logout_token, post_logout_redirect_uri).await - } } diff --git a/crates/iam/src/federation/provider.rs b/crates/iam/src/federation/provider.rs index 267c94f75..e59226c0b 100644 --- a/crates/iam/src/federation/provider.rs +++ b/crates/iam/src/federation/provider.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{FederatedAuthorization, FederatedCodeExchange, OpaqueLogoutContinuation, Result, VerifiedFederatedIdentity}; -use crate::oidc::OidcProviderConfig; +use super::{OpaqueLogoutContinuation, Result, VerifiedFederatedIdentity}; use serde::{Deserialize, Serialize}; /// Provider data exposed to login discovery consumers. @@ -56,41 +55,21 @@ impl VerifiedFederatedCodeExchange { #[async_trait::async_trait] pub(crate) trait StandardOidcAuthentication: Send + Sync { + async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option) -> Result; async fn exchange_identity(&self, state: &str, code: &str, redirect_uri: &str) -> Result; async fn verify_identity(&self, jwt: &str) -> Result; async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result; + async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result>; } -#[async_trait::async_trait] -pub trait FederatedIdentityProvider: Send + Sync { +pub(crate) trait FederatedProviderQuery: Send + Sync { fn has_providers(&self) -> bool; fn list_providers(&self) -> Vec; fn list_visible_providers(&self) -> Vec; - /// Compatibility accessor for existing provider implementations. - /// Login redirect consumers should use [`Self::redirect_policy`]. - fn provider_config(&self, _provider_id: &str) -> Option<&OidcProviderConfig> { - None - } - - fn redirect_policy(&self, provider_id: &str) -> Option { - self.provider_config(provider_id).map(|config| FederatedRedirectPolicy { - redirect_uri: config.redirect_uri.clone(), - allow_request_origin: config.redirect_uri_dynamic, - }) - } - - async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option) -> Result; - - async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result; - - async fn verify_web_identity_token(&self, jwt: &str) -> Result; - - async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result; - - async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result>; + fn redirect_policy(&self, provider_id: &str) -> Option; } #[cfg(test)] diff --git a/crates/iam/src/federation/registry.rs b/crates/iam/src/federation/registry.rs deleted file mode 100644 index bce7c93bf..000000000 --- a/crates/iam/src/federation/registry.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::FederatedIdentityProvider; -use std::sync::Arc; - -/// Compatibility facade for integrations built against the original provider API. -/// Production standard OIDC startup composes the adapter, mapper, and service directly. -pub struct FederatedIdentityRegistry { - standard_oidc: Arc, -} - -impl FederatedIdentityRegistry { - pub fn new(standard_oidc: Arc) -> Self { - Self { standard_oidc } - } - - pub(crate) fn standard_oidc_arc(&self) -> Arc { - Arc::clone(&self.standard_oidc) - } -} diff --git a/crates/iam/src/federation/transaction.rs b/crates/iam/src/federation/transaction.rs index 82e787ac3..b9bda054d 100644 --- a/crates/iam/src/federation/transaction.rs +++ b/crates/iam/src/federation/transaction.rs @@ -13,11 +13,10 @@ // limitations under the License. use super::{ - CoreFederatedAuthorizationMapper, FederatedAuthorization, FederatedIdentityProvider, FederatedIdentityRegistry, - FederatedLoginSession, FederatedProviderView, FederatedRedirectPolicy, FederatedSession, FederatedSessionBinding, - FederatedSessionTransaction, FederationError, OpaqueLogoutContinuation, Result, StandardOidcAuthentication, + CoreFederatedAuthorizationMapper, FederatedAuthorization, FederatedLoginSession, FederatedProviderQuery, + FederatedProviderView, FederatedRedirectPolicy, FederatedSession, FederatedSessionBinding, FederatedSessionTransaction, + FederationError, OpaqueLogoutContinuation, Result, StandardOidcAuthentication, }; -use crate::oidc::OidcProviderConfig; use std::sync::Arc; const DEFAULT_OIDC_PROVIDER_ID: &str = "default"; @@ -32,11 +31,7 @@ fn sorted_provider_views(mut providers: Vec) -> Vec, - standard_oidc: Option, -} - -struct StandardOidcRuntime { + provider_query: Arc, authentication: Arc, mapper: CoreFederatedAuthorizationMapper, } @@ -49,91 +44,57 @@ struct RuntimeCodeExchange { impl FederatedIdentityService { async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result { - match &self.standard_oidc { - Some(runtime) => { - let exchange = runtime.authentication.exchange_identity(state, code, redirect_uri).await?; - let (identity, redirect_after, logout_continuation) = exchange.into_parts(); - Ok(RuntimeCodeExchange { - authorization: runtime.mapper.map(identity), - redirect_after, - logout_continuation, - }) - } - None => { - let exchange = self.provider.exchange_code(state, code, redirect_uri).await?; - let (authorization, redirect_after, logout_continuation) = exchange.into_parts(); - Ok(RuntimeCodeExchange { - authorization, - redirect_after, - logout_continuation, - }) - } - } + let exchange = self.authentication.exchange_identity(state, code, redirect_uri).await?; + let (identity, redirect_after, logout_continuation) = exchange.into_parts(); + Ok(RuntimeCodeExchange { + authorization: self.mapper.map(identity), + redirect_after, + logout_continuation, + }) } async fn verify_identity(&self, jwt: &str) -> Result { - match &self.standard_oidc { - Some(runtime) => Ok(runtime.mapper.map(runtime.authentication.verify_identity(jwt).await?)), - None => self.provider.verify_web_identity_token(jwt).await, - } + Ok(self.mapper.map(self.authentication.verify_identity(jwt).await?)) } async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result { - match &self.standard_oidc { - Some(runtime) => runtime.authentication.create_logout_token(continuation).await, - None => { - let (provider, id_token) = continuation.into_parts(); - self.provider.create_logout_token(provider.as_str(), &id_token).await - } - } - } - - /// Build the compatibility service around the published provider interface. - pub fn new(registry: FederatedIdentityRegistry) -> Self { - Self { - provider: registry.standard_oidc_arc(), - standard_oidc: None, - } + self.authentication.create_logout_token(continuation).await } pub(super) fn from_standard_oidc_parts( - provider: Arc, + provider_query: Arc, authentication: Arc, mapper: CoreFederatedAuthorizationMapper, ) -> Self { Self { - provider, - standard_oidc: Some(StandardOidcRuntime { authentication, mapper }), + provider_query, + authentication, + mapper, } } pub fn has_providers(&self) -> bool { - self.provider.has_providers() + self.provider_query.has_providers() } pub fn list_providers(&self) -> Vec { - let providers = self.provider.list_providers(); + let providers = self.provider_query.list_providers(); sorted_provider_views(providers) } pub fn list_visible_providers(&self) -> Vec { - let providers = self.provider.list_visible_providers(); + let providers = self.provider_query.list_visible_providers(); sorted_provider_views(providers) } pub fn redirect_policy(&self, provider_id: &str) -> Option { - self.provider.redirect_policy(provider_id) - } - - /// Compatibility accessor for existing federation consumers. - /// Login redirects use [`Self::redirect_policy`], while OIDC-specific - /// configuration consumers use the OIDC configuration query. - pub fn get_provider_config(&self, provider_id: &str) -> Option<&OidcProviderConfig> { - self.provider.provider_config(provider_id) + self.provider_query.redirect_policy(provider_id) } pub async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option) -> Result { - self.provider.authorize_url(provider_id, redirect_uri, redirect_after).await + self.authentication + .authorize_url(provider_id, redirect_uri, redirect_after) + .await } pub async fn complete_authorization_code( @@ -205,7 +166,9 @@ impl FederatedIdentityService { } pub async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result> { - self.provider.build_logout_url(logout_token, post_logout_redirect_uri).await + self.authentication + .build_logout_url(logout_token, post_logout_redirect_uri) + .await } } @@ -213,9 +176,8 @@ impl FederatedIdentityService { mod tests { use super::*; use crate::federation::{ - FederatedAuthorization, FederatedAuthorizationRule, FederatedAuthorizationRules, FederatedClaims, FederatedCodeExchange, - FederatedIdentityProvider, FederatedProviderRef, FederatedSessionBindingError, VerifiedFederatedCodeExchange, - VerifiedFederatedIdentity, + FederatedAuthorizationRule, FederatedAuthorizationRules, FederatedClaims, FederatedProviderQuery, FederatedProviderRef, + FederatedSessionBindingError, VerifiedFederatedCodeExchange, VerifiedFederatedIdentity, }; use rustfs_credentials::Credentials; use std::sync::{Arc, Mutex}; @@ -235,6 +197,8 @@ mod tests { web_provider_id: &'static str, failure: ProviderFailure, events: Arc>>, + authorize_calls: Mutex)>>, + logout_url_calls: Mutex>, expected_logout: (&'static str, &'static str), listed_provider_ids: Vec<&'static str>, visible_provider_ids: Vec<&'static str>, @@ -249,6 +213,8 @@ mod tests { web_provider_id: DEFAULT_OIDC_PROVIDER_ID, failure: ProviderFailure::None, events, + authorize_calls: Mutex::new(Vec::new()), + logout_url_calls: Mutex::new(Vec::new()), expected_logout: (DEFAULT_OIDC_PROVIDER_ID, "id-token"), listed_provider_ids: Vec::new(), visible_provider_ids: Vec::new(), @@ -259,32 +225,6 @@ mod tests { self.events.lock().expect("event log should not be poisoned").push(event); } - fn authorization(&self, provider_id: &str) -> FederatedAuthorization { - FederatedAuthorization { - provider_id: provider_id.to_string(), - claims: FederatedClaims { - sub: "subject".to_string(), - email: String::new(), - username: "user".to_string(), - groups: vec!["source-group".to_string()], - raw: Default::default(), - }, - policies: if self.with_policy { - vec!["readwrite".to_string()] - } else { - Vec::new() - }, - group_claim_policies: Vec::new(), - groups: if self.with_group { - vec!["developers".to_string()] - } else { - Vec::new() - }, - roles_claim_key: None, - roles: Vec::new(), - } - } - fn identity(&self, provider_id: &str) -> VerifiedFederatedIdentity { VerifiedFederatedIdentity::from_claims( FederatedProviderRef::new(provider_id.to_string()), @@ -313,8 +253,7 @@ mod tests { .collect() } - #[async_trait::async_trait] - impl FederatedIdentityProvider for TestProvider { + impl FederatedProviderQuery for TestProvider { fn has_providers(&self) -> bool { true } @@ -333,52 +272,18 @@ mod tests { allow_request_origin: true, }) } - - async fn authorize_url( - &self, - _provider_id: &str, - _redirect_uri: &str, - _redirect_after: Option, - ) -> Result { - Ok("https://identity.example/authorize".to_string()) - } - - async fn exchange_code(&self, _state: &str, _code: &str, _redirect_uri: &str) -> Result { - self.record("exchange"); - if self.failure == ProviderFailure::Exchange { - return Err(FederationError::CodeExchange("exchange failed".to_string())); - } - Ok(FederatedCodeExchange { - authorization: self.authorization(self.browser_provider_id), - redirect_after: Some("/browser".to_string()), - id_token: "id-token".to_string(), - }) - } - - async fn verify_web_identity_token(&self, _jwt: &str) -> Result { - self.record("verify"); - if self.failure == ProviderFailure::Verification { - return Err(FederationError::TokenVerification("verification failed".to_string())); - } - Ok(self.authorization(self.web_provider_id)) - } - - async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result { - self.record("logout"); - assert_eq!((provider_id, id_token), self.expected_logout); - if self.failure == ProviderFailure::Logout { - return Err(FederationError::Logout("logout failed".to_string())); - } - Ok("logout-token".to_string()) - } - - async fn build_logout_url(&self, _logout_token: &str, _post_logout_redirect_uri: &str) -> Result> { - Ok(Some("https://identity.example/logout".to_string())) - } } #[async_trait::async_trait] impl StandardOidcAuthentication for TestProvider { + async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option) -> Result { + self.authorize_calls + .lock() + .expect("authorize call log should not be poisoned") + .push((provider_id.to_string(), redirect_uri.to_string(), redirect_after)); + Ok("https://identity.example/authorize".to_string()) + } + async fn exchange_identity( &self, _state: &str, @@ -414,6 +319,14 @@ mod tests { } Ok("logout-token".to_string()) } + + async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result> { + self.logout_url_calls + .lock() + .expect("logout URL call log should not be poisoned") + .push((logout_token.to_string(), post_logout_redirect_uri.to_string())); + Ok(Some("https://identity.example/logout".to_string())) + } } fn standard_service(provider: Arc) -> FederatedIdentityService { @@ -432,14 +345,12 @@ mod tests { ) })); let authentication: Arc = provider.clone(); - let provider: Arc = provider; - FederatedIdentityService { - provider, - standard_oidc: Some(StandardOidcRuntime { - authentication, - mapper: CoreFederatedAuthorizationMapper::new(rules), - }), - } + let provider_query: Arc = provider; + FederatedIdentityService::from_standard_oidc_parts( + provider_query, + authentication, + CoreFederatedAuthorizationMapper::new(rules), + ) } struct RecordingBinding { @@ -492,7 +403,7 @@ mod tests { let mut provider = TestProvider::new(events); provider.listed_provider_ids = vec!["zeta", "hidden", DEFAULT_OIDC_PROVIDER_ID, "alpha"]; provider.visible_provider_ids = vec!["zeta", DEFAULT_OIDC_PROVIDER_ID, "alpha"]; - let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(Arc::new(provider))); + let service = standard_service(Arc::new(provider)); assert_eq!( service @@ -519,6 +430,48 @@ mod tests { ); } + #[tokio::test] + async fn authorization_and_logout_urls_preserve_service_arguments() { + let provider = Arc::new(TestProvider::new(Arc::new(Mutex::new(Vec::new())))); + let service = standard_service(provider.clone()); + + let authorize_url = service + .authorize_url( + "corp", + "https://console.example.com/oauth/callback", + Some("/browser/path?tab=objects".to_string()), + ) + .await + .expect("authorization URL should be returned"); + let logout_url = service + .build_logout_url("opaque-logout-token", "https://console.example.com/signed-out") + .await + .expect("logout URL should be returned"); + + assert_eq!(authorize_url, "https://identity.example/authorize"); + assert_eq!(logout_url.as_deref(), Some("https://identity.example/logout")); + assert_eq!( + provider + .authorize_calls + .lock() + .expect("authorize call log should not be poisoned") + .as_slice(), + [( + "corp".to_string(), + "https://console.example.com/oauth/callback".to_string(), + Some("/browser/path?tab=objects".to_string()), + )] + ); + assert_eq!( + provider + .logout_url_calls + .lock() + .expect("logout URL call log should not be poisoned") + .as_slice(), + [("opaque-logout-token".to_string(), "https://console.example.com/signed-out".to_string(),)] + ); + } + #[tokio::test] async fn callback_and_web_identity_preserve_provider_and_transaction_boundaries() { let events = Arc::new(Mutex::new(Vec::new())); @@ -526,9 +479,8 @@ mod tests { provider.browser_provider_id = "corp"; provider.web_provider_id = "partner"; provider.expected_logout = ("corp", "id-token"); - let provider = Arc::new(provider); let binding = Arc::new(RecordingBinding::new(events.clone())); - let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(provider)); + let service = standard_service(Arc::new(provider)); let login = service .complete_authorization_code("state", "code", "https://console.example/callback", 3600, binding.as_ref()) @@ -604,76 +556,13 @@ mod tests { } #[tokio::test] - async fn standard_runtime_checks_authorization_before_binding() { + async fn web_identity_without_policy_or_group_is_not_bound() { let events = Arc::new(Mutex::new(Vec::new())); let mut provider = TestProvider::new(events.clone()); provider.with_policy = false; let binding = Arc::new(RecordingBinding::new(events.clone())); let service = standard_service(Arc::new(provider)); - let error = service - .assume_role_with_web_identity("jwt", 3600, None, binding.as_ref()) - .await - .expect_err("standard web identity requires mapped authorization"); - - assert!(matches!(error, FederationError::NoAuthorizationContext)); - assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), ["verify"]); - } - - #[tokio::test] - async fn standard_runtime_preserves_browser_failure_order() { - for (provider_failure, binding_failure, expected_events) in [ - (ProviderFailure::Exchange, false, vec!["exchange"]), - (ProviderFailure::None, true, vec!["exchange", "bind"]), - (ProviderFailure::Logout, false, vec!["exchange", "bind", "logout"]), - ] { - let events = Arc::new(Mutex::new(Vec::new())); - let mut provider = TestProvider::new(events.clone()); - provider.failure = provider_failure; - let mut binding = RecordingBinding::new(events.clone()); - binding.fail = binding_failure; - let service = standard_service(Arc::new(provider)); - - service - .complete_authorization_code("state", "code", "https://console.example/callback", 3600, &binding) - .await - .expect_err("the configured standard callback failure should be returned"); - - assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), expected_events); - } - } - - #[tokio::test] - async fn standard_runtime_preserves_web_identity_failure_order() { - for (provider_failure, binding_failure, expected_events) in [ - (ProviderFailure::Verification, false, vec!["verify"]), - (ProviderFailure::None, true, vec!["verify", "bind"]), - ] { - let events = Arc::new(Mutex::new(Vec::new())); - let mut provider = TestProvider::new(events.clone()); - provider.failure = provider_failure; - let mut binding = RecordingBinding::new(events.clone()); - binding.fail = binding_failure; - let service = standard_service(Arc::new(provider)); - - service - .assume_role_with_web_identity("jwt", 3600, None, &binding) - .await - .expect_err("the configured standard web identity failure should be returned"); - - assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), expected_events); - } - } - - #[tokio::test] - async fn web_identity_without_policy_or_group_is_not_bound() { - let events = Arc::new(Mutex::new(Vec::new())); - let mut provider = TestProvider::new(events.clone()); - provider.with_policy = false; - let provider = Arc::new(provider); - let binding = Arc::new(RecordingBinding::new(events.clone())); - let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(provider)); - let error = service .assume_role_with_web_identity("jwt", 3600, None, binding.as_ref()) .await @@ -689,16 +578,15 @@ mod tests { let mut provider = TestProvider::new(events.clone()); provider.with_policy = false; provider.with_group = true; - let provider = Arc::new(provider); let binding = Arc::new(RecordingBinding::new(events.clone())); - let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(provider)); + let service = standard_service(Arc::new(provider)); let session = service .assume_role_with_web_identity("jwt", 3600, None, binding.as_ref()) .await .expect("a mapped group is an authorization context"); - assert!(session.authorization.policies.is_empty()); + assert_eq!(session.authorization.policies, ["developers"]); assert_eq!(session.authorization.groups, ["developers"]); assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), ["verify", "bind"]); } @@ -716,7 +604,7 @@ mod tests { let mut binding = RecordingBinding::new(events.clone()); binding.fail = binding_failure; let binding = Arc::new(binding); - let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(Arc::new(provider))); + let service = standard_service(Arc::new(provider)); let error = service .complete_authorization_code("state", "code", "https://console.example/callback", 3600, binding.as_ref()) @@ -755,7 +643,7 @@ mod tests { let mut binding = RecordingBinding::new(events.clone()); binding.fail = binding_failure; let binding = Arc::new(binding); - let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(Arc::new(provider))); + let service = standard_service(Arc::new(provider)); let error = service .assume_role_with_web_identity("jwt", 3600, None, binding.as_ref()) diff --git a/crates/iam/src/oidc.rs b/crates/iam/src/oidc.rs index dc58c4bd3..7a4adaa2f 100644 --- a/crates/iam/src/oidc.rs +++ b/crates/iam/src/oidc.rs @@ -2354,8 +2354,9 @@ fn extract_canonical_group_values( groups } -#[cfg(test)] -pub(crate) fn make_test_sys(configs: Vec) -> OidcSys { +#[cfg(any(test, feature = "test-util"))] +#[doc(hidden)] +pub fn make_test_sys(configs: Vec) -> OidcSys { let configs = configs .into_iter() .map(test_sourced_config) @@ -2369,7 +2370,7 @@ pub(crate) fn make_test_sys(configs: Vec) -> OidcSys { } } -#[cfg(test)] +#[cfg(any(test, feature = "test-util"))] fn test_sourced_config(config: OidcProviderConfig) -> SourcedOidcProviderConfig { SourcedOidcProviderConfig { config, @@ -2377,8 +2378,9 @@ fn test_sourced_config(config: OidcProviderConfig) -> SourcedOidcProviderConfig } } -#[cfg(test)] -pub(crate) fn test_config(id: &str) -> OidcProviderConfig { +#[cfg(any(test, feature = "test-util"))] +#[doc(hidden)] +pub fn test_config(id: &str) -> OidcProviderConfig { OidcProviderConfig { id: id.to_string(), enabled: true, diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 9bc4de49e..04680f2cb 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -389,6 +389,7 @@ symbolic-demangle = { version = "=13.9.0", default-features = false, features = [dev-dependencies] uuid = { workspace = true, features = ["v4", "v5", "fast-rng", "macro-diagnostics"] } +rustfs-iam = { workspace = true, features = ["test-util"] } serial_test = { workspace = true } tempfile = { workspace = true } aws-config = { workspace = true } diff --git a/rustfs/src/admin/handlers/site_replication.rs b/rustfs/src/admin/handlers/site_replication.rs index 0ab9442c7..7adaf84b6 100644 --- a/rustfs/src/admin/handlers/site_replication.rs +++ b/rustfs/src/admin/handlers/site_replication.rs @@ -15,7 +15,7 @@ use crate::admin::auth::authorize_admin_request; use crate::admin::router::{AdminOperation, Operation, S3Router}; use crate::admin::runtime_sources::{ - current_deployment_id, current_federated_identity_runtime, current_iam_handle, current_object_store_handle, current_region, + current_deployment_id, current_iam_handle, current_object_store_handle, current_oidc_config_query, current_region, current_replication_pool_handle, current_replication_stats_handle, current_server_config, current_token_signing_key, object_store_from_req, }; @@ -2086,40 +2086,11 @@ fn open_id_provider_settings(config: &rustfs_iam::oidc::OidcProviderConfig) -> O } } -fn open_id_settings_from_service(service: &rustfs_iam::federation::FederatedIdentityService, region: String) -> OpenIDSettings { - let providers = service - .list_providers() - .into_iter() - .filter_map(|provider| { - let config = service.get_provider_config(&provider.provider_id)?; - Some((provider.provider_id, open_id_provider_settings(config))) - }) - .collect(); - - open_id_settings(providers, region) -} - -fn open_id_settings_from_runtime( - oidc_config_query: Option<&dyn rustfs_iam::federation::oidc::OidcConfigQuery>, - service: Option<&rustfs_iam::federation::FederatedIdentityService>, - region: String, -) -> OpenIDSettings { - if let Some(oidc_config_query) = oidc_config_query { - return open_id_settings_from_snapshot(&oidc_config_query.config_snapshot(), region); - } - - service - .map(|service| open_id_settings_from_service(service, region)) - .unwrap_or_default() -} - fn local_idp_settings() -> IDPSettings { let mut settings = IDPSettings::default(); let region = current_region().map(|region| region.to_string()).unwrap_or_default(); - settings.open_id = current_federated_identity_runtime() - .map(|(service, oidc_config_query)| { - open_id_settings_from_runtime(oidc_config_query.as_deref(), Some(service.as_ref()), region) - }) + settings.open_id = current_oidc_config_query() + .map(|query| open_id_settings_from_snapshot(&query.config_snapshot(), region)) .unwrap_or_default(); let (ldap, ldap_configs) = load_ldap_idp_settings(); @@ -11444,82 +11415,6 @@ mod tests { } } - struct LegacyOidcProvider { - configs: Vec, - } - - struct SnapshotOidcConfigQuery { - snapshot: rustfs_iam::oidc::OidcConfigSnapshot, - } - - impl rustfs_iam::federation::oidc::OidcConfigQuery for SnapshotOidcConfigQuery { - fn config_snapshot(&self) -> rustfs_iam::oidc::OidcConfigSnapshot { - self.snapshot.clone() - } - } - - #[async_trait::async_trait] - impl rustfs_iam::federation::FederatedIdentityProvider for LegacyOidcProvider { - fn has_providers(&self) -> bool { - !self.configs.is_empty() - } - - fn list_providers(&self) -> Vec { - self.configs - .iter() - .map(|config| rustfs_iam::federation::FederatedProviderView { - provider_id: config.id.clone(), - display_name: config.display_name.clone(), - }) - .collect() - } - - fn list_visible_providers(&self) -> Vec { - self.list_providers() - } - - fn provider_config(&self, provider_id: &str) -> Option<&rustfs_iam::oidc::OidcProviderConfig> { - self.configs.iter().find(|config| config.id == provider_id) - } - - async fn authorize_url( - &self, - _provider_id: &str, - _redirect_uri: &str, - _redirect_after: Option, - ) -> rustfs_iam::federation::Result { - unreachable!("site replication compatibility test does not authorize") - } - - async fn exchange_code( - &self, - _state: &str, - _code: &str, - _redirect_uri: &str, - ) -> rustfs_iam::federation::Result { - unreachable!("site replication compatibility test does not exchange codes") - } - - async fn verify_web_identity_token( - &self, - _jwt: &str, - ) -> rustfs_iam::federation::Result { - unreachable!("site replication compatibility test does not verify tokens") - } - - async fn create_logout_token(&self, _provider_id: &str, _id_token: &str) -> rustfs_iam::federation::Result { - unreachable!("site replication compatibility test does not create logout tokens") - } - - async fn build_logout_url( - &self, - _logout_token: &str, - _post_logout_redirect_uri: &str, - ) -> rustfs_iam::federation::Result> { - unreachable!("site replication compatibility test does not build logout URLs") - } - } - fn ldap_settings_kvs() -> rustfs_config::server_config::KVS { rustfs_config::server_config::KVS(vec![ rustfs_config::server_config::KV { @@ -11692,43 +11587,6 @@ mod tests { ); } - #[test] - fn service_only_oidc_runtime_preserves_site_replication_settings() { - let default = oidc_snapshot_provider("default", "client-a"); - let corp = oidc_snapshot_provider("corp", "client-b"); - let expected = open_id_settings_from_snapshot( - &rustfs_iam::oidc::OidcConfigSnapshot::new(vec![corp.clone(), default.clone()]), - "eu-site-1".to_string(), - ); - let service = rustfs_iam::federation::FederatedIdentityService::new( - rustfs_iam::federation::FederatedIdentityRegistry::new(Arc::new(LegacyOidcProvider { - configs: vec![corp.config, default.config], - })), - ); - - assert_eq!( - serde_json::to_value(open_id_settings_from_runtime(None, Some(&service), "eu-site-1".to_string(),)) - .expect("serialize legacy OpenID settings"), - serde_json::to_value(expected).expect("serialize snapshot OpenID settings"), - ); - } - - #[test] - fn oidc_query_takes_precedence_over_service_only_compatibility() { - let query = SnapshotOidcConfigQuery { - snapshot: rustfs_iam::oidc::OidcConfigSnapshot::new(vec![oidc_snapshot_provider("default", "query-client")]), - }; - let service = rustfs_iam::federation::FederatedIdentityService::new( - rustfs_iam::federation::FederatedIdentityRegistry::new(Arc::new(LegacyOidcProvider { - configs: vec![oidc_snapshot_provider("default", "service-client").config], - })), - ); - - let settings = open_id_settings_from_runtime(Some(&query), Some(&service), "eu-site-1".to_string()); - - assert_eq!(settings.claim_provider.client_id, "query-client"); - } - // A whole provider object present on one side only must not spill its // credential-derived leaves into the rejection message. #[test] diff --git a/rustfs/src/admin/runtime_sources.rs b/rustfs/src/admin/runtime_sources.rs index 513dfbbbf..655f43614 100644 --- a/rustfs/src/admin/runtime_sources.rs +++ b/rustfs/src/admin/runtime_sources.rs @@ -23,12 +23,11 @@ use crate::app::object_usecase::DefaultObjectUsecase; use crate::runtime_sources as root_runtime_sources; pub(crate) use crate::runtime_sources::{ AppContext, ServerContextSlot, current_action_credentials, current_boot_time, current_bucket_metadata_handle, - current_bucket_monitor_handle, current_deployment_id, current_endpoints_handle, current_federated_identity_runtime, - current_federated_identity_service, current_iam_handle, current_kms_runtime_service_manager, - current_notification_system_for_context, current_object_data_cache_handle_for_context, - current_object_store_handle_for_context, current_ready_iam_handle, current_region, current_replication_pool_handle, - current_replication_stats_handle, current_replication_stats_handle_for_context, current_server_config_for_context, - current_token_signing_key, + current_bucket_monitor_handle, current_deployment_id, current_endpoints_handle, current_federated_identity_service, + current_iam_handle, current_kms_runtime_service_manager, current_notification_system_for_context, + current_object_data_cache_handle_for_context, current_object_store_handle_for_context, current_oidc_config_query, + current_ready_iam_handle, current_region, current_replication_pool_handle, current_replication_stats_handle, + current_replication_stats_handle_for_context, current_server_config_for_context, current_token_signing_key, }; #[cfg(test)] pub(crate) use crate::runtime_sources::{ diff --git a/rustfs/src/app/context.rs b/rustfs/src/app/context.rs index ad69727d4..f1373cb46 100644 --- a/rustfs/src/app/context.rs +++ b/rustfs/src/app/context.rs @@ -103,11 +103,6 @@ pub fn resolve_oidc_config_query() -> Option> { resolve_oidc_config_query_with(get_global_app_context()) } -/// Resolve the paired federation service and OIDC configuration query from one runtime observation. -pub(crate) fn resolve_federated_identity_runtime() -> Option { - resolve_federated_identity_runtime_with(get_global_app_context()) -} - /// Publish the initialized federation service and OIDC configuration query. pub fn publish_federated_identity_runtime( service: Arc, @@ -116,11 +111,6 @@ pub fn publish_federated_identity_runtime( handles::publish_default_federated_identity_runtime(service, oidc_config_query) } -/// Publish a service-only federation runtime for compatibility integrations. -pub fn publish_federated_identity_service(service: Arc) -> bool { - handles::publish_default_federated_identity_service(service) -} - /// Resolve a ready IAM system handle using AppContext-first precedence. pub fn resolve_ready_iam_handle() -> rustfs_iam::error::Result>> { if let Some(context) = get_global_app_context() { @@ -360,6 +350,7 @@ fn resolve_oidc_config_query_with(context: Option>) -> Option>) -> Option { context.and_then(|context| context.federated_identity().runtime_snapshot()) } @@ -554,7 +545,7 @@ mod tests { use async_trait::async_trait; use rustfs_iam::{ federation::{ - FederatedIdentityRegistry, FederatedIdentityService, + CoreFederatedAuthorizationMapper, FederatedIdentityService, oidc::{OidcConfigQuery, StandardOidcAdapter}, }, oidc::OidcSys, @@ -617,25 +608,11 @@ mod tests { } impl FederatedIdentityInterface for TestFederatedIdentityInterface { - fn handle(&self) -> Option> { - self.runtime - .read() - .ok() - .and_then(|runtime| runtime.as_ref().map(|runtime| Arc::clone(&runtime.service))) - } - - fn oidc_config_query(&self) -> Option> { - self.runtime - .read() - .ok() - .and_then(|runtime| runtime.as_ref().map(|runtime| Arc::clone(&runtime.oidc_config_query))) - } - fn runtime_snapshot(&self) -> Option { self.runtime.read().ok().and_then(|runtime| { runtime .as_ref() - .map(|runtime| (Arc::clone(&runtime.service), Some(Arc::clone(&runtime.oidc_config_query)))) + .map(|runtime| (Arc::clone(&runtime.service), Arc::clone(&runtime.oidc_config_query))) }) } @@ -654,7 +631,8 @@ mod tests { fn test_federated_identity_runtime(oidc: OidcSys) -> (Arc, Arc) { let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc))); let oidc_config_query = adapter.clone(); - let service = Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter))); + let mapper = CoreFederatedAuthorizationMapper::new(adapter.authorization_rules()); + let service = Arc::new(adapter.into_service(mapper)); (service, oidc_config_query) } @@ -1206,11 +1184,7 @@ mod tests { let (resolved_oidc, resolved_oidc_query) = resolve_federated_identity_runtime_with(Some(context.clone())).expect("context federated identity runtime"); assert!(Arc::ptr_eq(&resolved_oidc, &context_oidc)); - assert!( - resolved_oidc_query - .as_ref() - .is_some_and(|query| Arc::ptr_eq(query, &context_oidc_query)) - ); + assert!(Arc::ptr_eq(&resolved_oidc_query, &context_oidc_query)); assert!(publish_federated_identity_runtime_with( Some(context.clone()), fallback_oidc.clone(), diff --git a/rustfs/src/app/context/global.rs b/rustfs/src/app/context/global.rs index b5a8047cc..fe52e40db 100644 --- a/rustfs/src/app/context/global.rs +++ b/rustfs/src/app/context/global.rs @@ -169,11 +169,6 @@ impl AppContext { self.federated_identity.publish_runtime(service, oidc_config_query) } - /// Publish a service-only federation runtime for compatibility integrations. - pub fn publish_federated_identity_service(&self, service: Arc) -> bool { - self.federated_identity.publish_handle(service) - } - pub fn kms(&self) -> Arc { self.kms.clone() } diff --git a/rustfs/src/app/context/handles.rs b/rustfs/src/app/context/handles.rs index f2c9d0944..6fbbddc50 100644 --- a/rustfs/src/app/context/handles.rs +++ b/rustfs/src/app/context/handles.rs @@ -87,24 +87,15 @@ pub struct FederatedIdentityHandle { struct FederatedIdentityRuntime { service: Arc, - oidc_config_query: Option>, + oidc_config_query: Arc, } impl FederatedIdentityHandle { - pub fn new(service: Option>) -> Self { - Self { - runtime: StdRwLock::new(service.map(|service| FederatedIdentityRuntime { - service, - oidc_config_query: None, - })), - } - } - pub fn with_runtime(runtime: Option<(Arc, Arc)>) -> Self { Self { runtime: StdRwLock::new(runtime.map(|(service, oidc_config_query)| FederatedIdentityRuntime { service, - oidc_config_query: Some(oidc_config_query), + oidc_config_query, })), } } @@ -112,45 +103,26 @@ impl FederatedIdentityHandle { impl Default for FederatedIdentityHandle { fn default() -> Self { - Self::new(None) + Self::with_runtime(None) } } impl FederatedIdentityInterface for FederatedIdentityHandle { - fn handle(&self) -> Option> { - self.runtime_snapshot().map(|(service, _)| service) - } - - fn oidc_config_query(&self) -> Option> { - self.runtime_snapshot().and_then(|(_, oidc_config_query)| oidc_config_query) - } - fn runtime_snapshot(&self) -> Option { self.runtime.read().ok().and_then(|runtime| { runtime .as_ref() - .map(|runtime| (Arc::clone(&runtime.service), runtime.oidc_config_query.as_ref().map(Arc::clone))) + .map(|runtime| (Arc::clone(&runtime.service), Arc::clone(&runtime.oidc_config_query))) }) } - fn publish_handle(&self, service: Arc) -> bool { - let Ok(mut published_runtime) = self.runtime.write() else { - return false; - }; - *published_runtime = Some(FederatedIdentityRuntime { - service, - oidc_config_query: None, - }); - true - } - fn publish_runtime(&self, service: Arc, oidc_config_query: Arc) -> bool { let Ok(mut published_runtime) = self.runtime.write() else { return false; }; *published_runtime = Some(FederatedIdentityRuntime { service, - oidc_config_query: Some(oidc_config_query), + oidc_config_query, }); true } @@ -639,15 +611,6 @@ pub fn publish_default_federated_identity_runtime( default_federated_identity_handle().publish_runtime(service, oidc_config_query) } -/// Publish a service-only federation runtime for compatibility integrations. -pub fn publish_default_federated_identity_service(service: Arc) -> bool { - default_federated_identity_handle().publish_handle(service) -} - -pub fn federated_identity_interface(service: Option>) -> Arc { - Arc::new(FederatedIdentityHandle::new(service)) -} - pub fn federated_identity_runtime_interface( runtime: Option<(Arc, Arc)>, ) -> Arc { @@ -686,14 +649,13 @@ pub fn default_buffer_config_interface() -> Arc { mod tests { use super::{ KmsRuntimeHandle, KmsServiceManager, ServerConfigHandle, default_federated_identity_interface, - federated_identity_interface, federated_identity_runtime_interface, publish_default_federated_identity_runtime, - runtime_sources, + federated_identity_runtime_interface, publish_default_federated_identity_runtime, runtime_sources, }; use crate::app::context::interfaces::{KmsRuntimeInterface, ServerConfigInterface}; use rustfs_config::server_config::Config; use rustfs_iam::{ federation::{ - FederatedIdentityRegistry, FederatedIdentityService, + CoreFederatedAuthorizationMapper, FederatedIdentityService, oidc::{OidcConfigQuery, StandardOidcAdapter}, }, oidc::OidcSys, @@ -705,7 +667,8 @@ mod tests { let oidc = OidcSys::empty().expect("empty OIDC configuration should be valid"); let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc))); let oidc_config_query = adapter.clone(); - let service = Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter))); + let mapper = CoreFederatedAuthorizationMapper::new(adapter.authorization_rules()); + let service = Arc::new(adapter.into_service(mapper)); (service, oidc_config_query) } @@ -726,7 +689,7 @@ mod tests { assert!(Arc::ptr_eq(&resolved_query, &first_query)); let (resolved_service, resolved_query) = interface.runtime_snapshot().expect("initial federated identity runtime"); assert!(Arc::ptr_eq(&resolved_service, &first)); - assert!(resolved_query.as_ref().is_some_and(|query| Arc::ptr_eq(query, &first_query))); + assert!(Arc::ptr_eq(&resolved_query, &first_query)); assert!(interface.publish_runtime(second.clone(), second_query.clone())); assert!( @@ -738,36 +701,11 @@ mod tests { assert!(interface.oidc_config_query().is_some()); let (resolved_service, resolved_query) = interface.runtime_snapshot().expect("replacement federated identity runtime"); assert!(Arc::ptr_eq(&resolved_service, &second)); - assert!(resolved_query.as_ref().is_some_and(|query| Arc::ptr_eq(query, &second_query))); - } - - #[test] - fn federated_identity_handle_preserves_service_only_compatibility() { - let (service, _) = test_federated_identity_runtime(); - let (replacement, _) = test_federated_identity_runtime(); - let interface = federated_identity_interface(Some(service.clone())); - - assert!( - interface - .handle() - .as_ref() - .is_some_and(|resolved| Arc::ptr_eq(resolved, &service)) - ); - assert!(interface.oidc_config_query().is_none()); - assert!(interface.publish_handle(replacement.clone())); - assert!( - interface - .handle() - .as_ref() - .is_some_and(|resolved| Arc::ptr_eq(resolved, &replacement)) - ); - assert!(interface.oidc_config_query().is_none()); - let (resolved_service, resolved_query) = interface.runtime_snapshot().expect("service-only federated identity runtime"); - assert!(Arc::ptr_eq(&resolved_service, &replacement)); - assert!(resolved_query.is_none()); + assert!(Arc::ptr_eq(&resolved_query, &second_query)); } #[test] + #[serial_test::serial(federated_identity_runtime)] fn default_federated_identity_handle_shares_early_publish_and_replacement() { let (first, first_query) = test_federated_identity_runtime(); let (second, second_query) = test_federated_identity_runtime(); diff --git a/rustfs/src/app/context/interfaces.rs b/rustfs/src/app/context/interfaces.rs index ac136ee11..8fd9eb7bf 100644 --- a/rustfs/src/app/context/interfaces.rs +++ b/rustfs/src/app/context/interfaces.rs @@ -48,25 +48,22 @@ pub trait IamInterface: Send + Sync { } /// Published federation runtime handles for authentication and OIDC configuration consumers. -pub type FederatedIdentityRuntimeSnapshot = (Arc, Option>); +pub type FederatedIdentityRuntimeSnapshot = (Arc, Arc); pub trait FederatedIdentityInterface: Send + Sync { - fn handle(&self) -> Option>; + fn runtime_snapshot(&self) -> Option; + + fn handle(&self) -> Option> { + self.runtime_snapshot().map(|(service, _)| service) + } + fn oidc_config_query(&self) -> Option> { - None + self.runtime_snapshot().map(|(_, oidc_config_query)| oidc_config_query) } - fn runtime_snapshot(&self) -> Option { - self.handle().map(|service| (service, self.oidc_config_query())) - } - /// Compatibility publication hook for service-only implementations. - /// Standard OIDC runtimes publish the paired query through - /// [`Self::publish_runtime`]. - fn publish_handle(&self, _service: Arc) -> bool { + + fn publish_runtime(&self, _service: Arc, _oidc_config_query: Arc) -> bool { false } - fn publish_runtime(&self, service: Arc, _oidc_config_query: Arc) -> bool { - self.publish_handle(service) - } } /// KMS interface for application-layer use-cases. diff --git a/rustfs/src/runtime_sources.rs b/rustfs/src/runtime_sources.rs index e685878a3..fc211890b 100644 --- a/rustfs/src/runtime_sources.rs +++ b/rustfs/src/runtime_sources.rs @@ -35,7 +35,6 @@ pub(crate) use context::{ resolve_bucket_monitor_handle as current_bucket_monitor_handle, resolve_buffer_config as current_buffer_config, resolve_daily_tier_stats as current_daily_tier_stats, resolve_deployment_id as current_deployment_id, resolve_encryption_service as current_encryption_service, resolve_endpoints_handle as current_endpoints_handle, - resolve_federated_identity_runtime as current_federated_identity_runtime, resolve_federated_identity_service as current_federated_identity_service, resolve_iam_handle as current_iam_handle, resolve_iam_ready as current_iam_ready, resolve_internode_metrics as current_internode_metrics, resolve_kms_runtime_service_manager as current_kms_runtime_service_manager, @@ -47,6 +46,7 @@ pub(crate) use context::{ resolve_object_data_cache_handle_for_context as current_object_data_cache_handle_for_context, resolve_object_store_handle as current_object_store_handle, resolve_object_store_handle_for_context as current_object_store_handle_for_context, + resolve_oidc_config_query as current_oidc_config_query, resolve_or_init_kms_runtime_service_manager as current_or_init_kms_runtime_service_manager, resolve_outbound_tls_generation as current_outbound_tls_generation, resolve_outbound_tls_state as current_outbound_tls_state, resolve_performance_metrics as current_performance_metrics, resolve_ready_iam_handle as current_ready_iam_handle, @@ -63,13 +63,13 @@ pub(crate) fn publish_federated_identity_runtime( service: Arc, oidc_config_query: Arc, ) -> bool { - warn_on_browser_redirect_fallback(&service); + warn_on_browser_redirect_fallback(service.has_providers()); context::publish_federated_identity_runtime(service, oidc_config_query) } -fn warn_on_browser_redirect_fallback(service: &FederatedIdentityService) { +fn warn_on_browser_redirect_fallback(has_providers: bool) { let browser_redirect_url = rustfs_utils::get_env_opt_str(ENV_RUSTFS_BROWSER_REDIRECT_URL); - if service.has_providers() && browser_redirect_url.as_deref().is_none_or(|value| value.trim().is_empty()) { + if has_providers && browser_redirect_url.as_deref().is_none_or(|value| value.trim().is_empty()) { warn!( event = EVENT_OIDC_BROWSER_REDIRECT_FALLBACK, component = LOG_COMPONENT_MAIN, @@ -103,76 +103,19 @@ pub(crate) fn current_app_context() -> Option> { #[cfg(test)] mod tests { use super::*; - use rustfs_iam::federation::oidc::OidcConfigQuery; - use rustfs_iam::federation::{ - FederatedAuthorization, FederatedCodeExchange, FederatedIdentityProvider, FederatedIdentityRegistry, - FederatedProviderView, FederatedRedirectPolicy, Result as FederationResult, + use rustfs_iam::{ + federation::{ + CoreFederatedAuthorizationMapper, + oidc::{OidcConfigQuery, StandardOidcAdapter}, + }, + oidc::{OidcSys, make_test_sys, test_config}, }; - use rustfs_iam::oidc::OidcConfigSnapshot; use std::{ io::{self, Write}, sync::Mutex, }; use tracing_subscriber::{Registry, fmt::MakeWriter, layer::SubscriberExt}; - struct TestProvider { - has_providers: bool, - } - - #[async_trait::async_trait] - impl FederatedIdentityProvider for TestProvider { - fn has_providers(&self) -> bool { - self.has_providers - } - - fn list_providers(&self) -> Vec { - Vec::new() - } - - fn list_visible_providers(&self) -> Vec { - Vec::new() - } - - fn redirect_policy(&self, _provider_id: &str) -> Option { - None - } - - async fn authorize_url( - &self, - _provider_id: &str, - _redirect_uri: &str, - _redirect_after: Option, - ) -> FederationResult { - unreachable!("startup publication test does not authorize") - } - - async fn exchange_code(&self, _state: &str, _code: &str, _redirect_uri: &str) -> FederationResult { - unreachable!("startup publication test does not exchange codes") - } - - async fn verify_web_identity_token(&self, _jwt: &str) -> FederationResult { - unreachable!("startup publication test does not verify tokens") - } - - async fn create_logout_token(&self, _provider_id: &str, _id_token: &str) -> FederationResult { - unreachable!("startup publication test does not create logout tokens") - } - - async fn build_logout_url( - &self, - _logout_token: &str, - _post_logout_redirect_uri: &str, - ) -> FederationResult> { - unreachable!("startup publication test does not build logout URLs") - } - } - - impl OidcConfigQuery for TestProvider { - fn config_snapshot(&self) -> OidcConfigSnapshot { - OidcConfigSnapshot::new(Vec::new()) - } - } - #[derive(Clone, Default)] struct CapturedLogs(Arc>>); @@ -197,14 +140,14 @@ mod tests { } } - fn test_runtime(has_providers: bool) -> (Arc, Arc) { - let provider = Arc::new(TestProvider { has_providers }); - let service = Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(provider.clone()))); - (service, provider) - } - - fn capture_startup_publication(has_providers: bool, browser_redirect_url: Option<&str>) -> String { + fn capture_startup_publication(oidc: OidcSys, browser_redirect_url: Option<&str>) -> String { temp_env::with_var(ENV_RUSTFS_BROWSER_REDIRECT_URL, browser_redirect_url, || { + let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc))); + let oidc_config_query: Arc = adapter.clone(); + let mapper = CoreFederatedAuthorizationMapper::new(adapter.authorization_rules()); + let service = Arc::new(adapter.into_service(mapper)); + let expected_service = service.clone(); + let expected_oidc_config_query = oidc_config_query.clone(); let logs = CapturedLogs::default(); let captured = logs.0.clone(); let subscriber = Registry::default().with( @@ -217,8 +160,12 @@ mod tests { ); tracing::subscriber::with_default(subscriber, || { - let (service, oidc_config_query) = test_runtime(has_providers); assert!(publish_federated_identity_runtime(service, oidc_config_query)); + let (published_service, published_oidc_config_query) = context::default_federated_identity_interface() + .runtime_snapshot() + .expect("published federated identity runtime should resolve"); + assert!(Arc::ptr_eq(&published_service, &expected_service)); + assert!(Arc::ptr_eq(&published_oidc_config_query, &expected_oidc_config_query)); }); String::from_utf8(captured.lock().expect("captured log lock").clone()).expect("captured logs must be UTF-8") @@ -226,16 +173,20 @@ mod tests { } #[test] + #[serial_test::serial(federated_identity_runtime)] fn oidc_startup_publication_warns_only_for_request_header_fallback() { - let output = capture_startup_publication(true, None); + let output = capture_startup_publication(make_test_sys(vec![test_config("default")]), None); assert!(output.contains("WARN"), "{output}"); assert!(output.contains(EVENT_OIDC_BROWSER_REDIRECT_FALLBACK), "{output}"); assert!(output.contains(ENV_RUSTFS_BROWSER_REDIRECT_URL), "{output}"); assert!(output.contains("request_host_and_forwarded_proto"), "{output}"); assert!(output.contains("trusted_console_origin_not_configured"), "{output}"); - assert!(capture_startup_publication(false, None).is_empty()); - assert!(capture_startup_publication(true, Some("https://console.example.com")).is_empty()); - assert!(!capture_startup_publication(true, Some(" ")).is_empty()); + assert!(capture_startup_publication(OidcSys::empty().expect("empty OIDC system should initialize"), None).is_empty()); + assert!( + capture_startup_publication(make_test_sys(vec![test_config("default")]), Some("https://console.example.com"),) + .is_empty() + ); + assert!(!capture_startup_publication(make_test_sys(vec![test_config("default")]), Some(" ")).is_empty()); } }