refactor(iam): remove legacy OIDC federation paths (#8433)

Co-authored-by: Hauser <housemecn@gmail.com>
This commit is contained in:
GatewayJ
2026-10-09 22:11:50 +08:00
committed by GitHub
parent 99af59a914
commit 446479791e
16 changed files with 205 additions and 731 deletions
+1
View File
@@ -30,6 +30,7 @@ workspace = true
[features]
default = []
test-util = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
+4 -6
View File
@@ -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;
+2 -50
View File
@@ -164,40 +164,6 @@ impl FederatedAuthorization {
}
}
pub struct FederatedCodeExchange {
pub authorization: FederatedAuthorization,
pub redirect_after: Option<String>,
pub id_token: String,
}
impl FederatedCodeExchange {
pub(crate) fn into_parts(self) -> (FederatedAuthorization, Option<String>, 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]"));
}
+14 -41
View File
@@ -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<Self>, mapper: CoreFederatedAuthorizationMapper) -> FederatedIdentityService {
let authentication: Arc<dyn StandardOidcAuthentication> = self.clone();
let provider: Arc<dyn FederatedIdentityProvider> = self;
FederatedIdentityService::from_standard_oidc_parts(provider, authentication, mapper)
let provider_query: Arc<dyn FederatedProviderQuery> = 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<String>) -> Result<String> {
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<VerifiedFederatedCodeExchange> {
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<Option<String>> {
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<FederatedRedirectPolicy> {
config::redirect_policy(&self.oidc, provider_id)
}
async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option<String>) -> Result<String> {
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<FederatedCodeExchange> {
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<FederatedAuthorization> {
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<String> {
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<Option<String>> {
http::build_logout_url(&self.oidc, logout_token, post_logout_redirect_uri).await
}
}
+5 -26
View File
@@ -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<String>) -> Result<String>;
async fn exchange_identity(&self, state: &str, code: &str, redirect_uri: &str) -> Result<VerifiedFederatedCodeExchange>;
async fn verify_identity(&self, jwt: &str) -> Result<VerifiedFederatedIdentity>;
async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result<String>;
async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result<Option<String>>;
}
#[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<FederatedProviderView>;
fn list_visible_providers(&self) -> Vec<FederatedProviderView>;
/// 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<FederatedRedirectPolicy> {
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<String>) -> Result<String>;
async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result<FederatedCodeExchange>;
async fn verify_web_identity_token(&self, jwt: &str) -> Result<FederatedAuthorization>;
async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result<String>;
async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result<Option<String>>;
fn redirect_policy(&self, provider_id: &str) -> Option<FederatedRedirectPolicy>;
}
#[cfg(test)]
-32
View File
@@ -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<dyn FederatedIdentityProvider>,
}
impl FederatedIdentityRegistry {
pub fn new(standard_oidc: Arc<dyn FederatedIdentityProvider>) -> Self {
Self { standard_oidc }
}
pub(crate) fn standard_oidc_arc(&self) -> Arc<dyn FederatedIdentityProvider> {
Arc::clone(&self.standard_oidc)
}
}
+105 -217
View File
@@ -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<FederatedProviderView>) -> Vec<Feder
}
pub struct FederatedIdentityService {
provider: Arc<dyn FederatedIdentityProvider>,
standard_oidc: Option<StandardOidcRuntime>,
}
struct StandardOidcRuntime {
provider_query: Arc<dyn FederatedProviderQuery>,
authentication: Arc<dyn StandardOidcAuthentication>,
mapper: CoreFederatedAuthorizationMapper,
}
@@ -49,91 +44,57 @@ struct RuntimeCodeExchange {
impl FederatedIdentityService {
async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result<RuntimeCodeExchange> {
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<FederatedAuthorization> {
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<String> {
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<dyn FederatedIdentityProvider>,
provider_query: Arc<dyn FederatedProviderQuery>,
authentication: Arc<dyn StandardOidcAuthentication>,
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<FederatedProviderView> {
let providers = self.provider.list_providers();
let providers = self.provider_query.list_providers();
sorted_provider_views(providers)
}
pub fn list_visible_providers(&self) -> Vec<FederatedProviderView> {
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<FederatedRedirectPolicy> {
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<String>) -> Result<String> {
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<Option<String>> {
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<Mutex<Vec<&'static str>>>,
authorize_calls: Mutex<Vec<(String, String, Option<String>)>>,
logout_url_calls: Mutex<Vec<(String, String)>>,
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<String>,
) -> Result<String> {
Ok("https://identity.example/authorize".to_string())
}
async fn exchange_code(&self, _state: &str, _code: &str, _redirect_uri: &str) -> Result<FederatedCodeExchange> {
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<FederatedAuthorization> {
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<String> {
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<Option<String>> {
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<String>) -> Result<String> {
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<Option<String>> {
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<TestProvider>) -> FederatedIdentityService {
@@ -432,14 +345,12 @@ mod tests {
)
}));
let authentication: Arc<dyn StandardOidcAuthentication> = provider.clone();
let provider: Arc<dyn FederatedIdentityProvider> = provider;
FederatedIdentityService {
provider,
standard_oidc: Some(StandardOidcRuntime {
authentication,
mapper: CoreFederatedAuthorizationMapper::new(rules),
}),
}
let provider_query: Arc<dyn FederatedProviderQuery> = 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())
+7 -5
View File
@@ -2354,8 +2354,9 @@ fn extract_canonical_group_values(
groups
}
#[cfg(test)]
pub(crate) fn make_test_sys(configs: Vec<OidcProviderConfig>) -> OidcSys {
#[cfg(any(test, feature = "test-util"))]
#[doc(hidden)]
pub fn make_test_sys(configs: Vec<OidcProviderConfig>) -> OidcSys {
let configs = configs
.into_iter()
.map(test_sourced_config)
@@ -2369,7 +2370,7 @@ pub(crate) fn make_test_sys(configs: Vec<OidcProviderConfig>) -> 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,
+1
View File
@@ -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 }
+3 -145
View File
@@ -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<rustfs_iam::oidc::OidcProviderConfig>,
}
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<rustfs_iam::federation::FederatedProviderView> {
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<rustfs_iam::federation::FederatedProviderView> {
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<String>,
) -> rustfs_iam::federation::Result<String> {
unreachable!("site replication compatibility test does not authorize")
}
async fn exchange_code(
&self,
_state: &str,
_code: &str,
_redirect_uri: &str,
) -> rustfs_iam::federation::Result<rustfs_iam::federation::FederatedCodeExchange> {
unreachable!("site replication compatibility test does not exchange codes")
}
async fn verify_web_identity_token(
&self,
_jwt: &str,
) -> rustfs_iam::federation::Result<rustfs_iam::federation::FederatedAuthorization> {
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<String> {
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<Option<String>> {
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]
+5 -6
View File
@@ -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::{
+6 -32
View File
@@ -103,11 +103,6 @@ pub fn resolve_oidc_config_query() -> Option<Arc<dyn OidcConfigQuery>> {
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<FederatedIdentityRuntimeSnapshot> {
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<FederatedIdentityService>,
@@ -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<FederatedIdentityService>) -> 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<Arc<IamSys<ObjectStore>>> {
if let Some(context) = get_global_app_context() {
@@ -360,6 +350,7 @@ fn resolve_oidc_config_query_with(context: Option<Arc<AppContext>>) -> Option<Ar
context.and_then(|context| context.federated_identity().oidc_config_query())
}
#[cfg(test)]
fn resolve_federated_identity_runtime_with(context: Option<Arc<AppContext>>) -> Option<FederatedIdentityRuntimeSnapshot> {
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<Arc<FederatedIdentityService>> {
self.runtime
.read()
.ok()
.and_then(|runtime| runtime.as_ref().map(|runtime| Arc::clone(&runtime.service)))
}
fn oidc_config_query(&self) -> Option<Arc<dyn OidcConfigQuery>> {
self.runtime
.read()
.ok()
.and_then(|runtime| runtime.as_ref().map(|runtime| Arc::clone(&runtime.oidc_config_query)))
}
fn runtime_snapshot(&self) -> Option<FederatedIdentityRuntimeSnapshot> {
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<FederatedIdentityService>, Arc<dyn OidcConfigQuery>) {
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(),
-5
View File
@@ -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<FederatedIdentityService>) -> bool {
self.federated_identity.publish_handle(service)
}
pub fn kms(&self) -> Arc<dyn KmsInterface> {
self.kms.clone()
}
+12 -74
View File
@@ -87,24 +87,15 @@ pub struct FederatedIdentityHandle {
struct FederatedIdentityRuntime {
service: Arc<FederatedIdentityService>,
oidc_config_query: Option<Arc<dyn OidcConfigQuery>>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
}
impl FederatedIdentityHandle {
pub fn new(service: Option<Arc<FederatedIdentityService>>) -> Self {
Self {
runtime: StdRwLock::new(service.map(|service| FederatedIdentityRuntime {
service,
oidc_config_query: None,
})),
}
}
pub fn with_runtime(runtime: Option<(Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>)>) -> 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<Arc<FederatedIdentityService>> {
self.runtime_snapshot().map(|(service, _)| service)
}
fn oidc_config_query(&self) -> Option<Arc<dyn OidcConfigQuery>> {
self.runtime_snapshot().and_then(|(_, oidc_config_query)| oidc_config_query)
}
fn runtime_snapshot(&self) -> Option<FederatedIdentityRuntimeSnapshot> {
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<FederatedIdentityService>) -> 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<FederatedIdentityService>, oidc_config_query: Arc<dyn OidcConfigQuery>) -> 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<FederatedIdentityService>) -> bool {
default_federated_identity_handle().publish_handle(service)
}
pub fn federated_identity_interface(service: Option<Arc<FederatedIdentityService>>) -> Arc<dyn FederatedIdentityInterface> {
Arc::new(FederatedIdentityHandle::new(service))
}
pub fn federated_identity_runtime_interface(
runtime: Option<(Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>)>,
) -> Arc<dyn FederatedIdentityInterface> {
@@ -686,14 +649,13 @@ pub fn default_buffer_config_interface() -> Arc<dyn BufferConfigInterface> {
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();
+10 -13
View File
@@ -48,25 +48,22 @@ pub trait IamInterface: Send + Sync {
}
/// Published federation runtime handles for authentication and OIDC configuration consumers.
pub type FederatedIdentityRuntimeSnapshot = (Arc<FederatedIdentityService>, Option<Arc<dyn OidcConfigQuery>>);
pub type FederatedIdentityRuntimeSnapshot = (Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>);
pub trait FederatedIdentityInterface: Send + Sync {
fn handle(&self) -> Option<Arc<FederatedIdentityService>>;
fn runtime_snapshot(&self) -> Option<FederatedIdentityRuntimeSnapshot>;
fn handle(&self) -> Option<Arc<FederatedIdentityService>> {
self.runtime_snapshot().map(|(service, _)| service)
}
fn oidc_config_query(&self) -> Option<Arc<dyn OidcConfigQuery>> {
None
self.runtime_snapshot().map(|(_, oidc_config_query)| oidc_config_query)
}
fn runtime_snapshot(&self) -> Option<FederatedIdentityRuntimeSnapshot> {
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<FederatedIdentityService>) -> bool {
fn publish_runtime(&self, _service: Arc<FederatedIdentityService>, _oidc_config_query: Arc<dyn OidcConfigQuery>) -> bool {
false
}
fn publish_runtime(&self, service: Arc<FederatedIdentityService>, _oidc_config_query: Arc<dyn OidcConfigQuery>) -> bool {
self.publish_handle(service)
}
}
/// KMS interface for application-layer use-cases.
+30 -79
View File
@@ -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<FederatedIdentityService>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
) -> 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<Arc<AppContext>> {
#[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<FederatedProviderView> {
Vec::new()
}
fn list_visible_providers(&self) -> Vec<FederatedProviderView> {
Vec::new()
}
fn redirect_policy(&self, _provider_id: &str) -> Option<FederatedRedirectPolicy> {
None
}
async fn authorize_url(
&self,
_provider_id: &str,
_redirect_uri: &str,
_redirect_after: Option<String>,
) -> FederationResult<String> {
unreachable!("startup publication test does not authorize")
}
async fn exchange_code(&self, _state: &str, _code: &str, _redirect_uri: &str) -> FederationResult<FederatedCodeExchange> {
unreachable!("startup publication test does not exchange codes")
}
async fn verify_web_identity_token(&self, _jwt: &str) -> FederationResult<FederatedAuthorization> {
unreachable!("startup publication test does not verify tokens")
}
async fn create_logout_token(&self, _provider_id: &str, _id_token: &str) -> FederationResult<String> {
unreachable!("startup publication test does not create logout tokens")
}
async fn build_logout_url(
&self,
_logout_token: &str,
_post_logout_redirect_uri: &str,
) -> FederationResult<Option<String>> {
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<Mutex<Vec<u8>>>);
@@ -197,14 +140,14 @@ mod tests {
}
}
fn test_runtime(has_providers: bool) -> (Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>) {
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<dyn OidcConfigQuery> = 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());
}
}