mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-12 23:52:45 +00:00
refactor(iam): remove legacy OIDC federation paths (#8433)
Co-authored-by: Hauser <housemecn@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test-util = []
|
||||
hotpath = [
|
||||
"hotpath/hotpath",
|
||||
"hotpath/tokio",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user