refactor(iam): separate federated provider views (#8415)

This commit is contained in:
GatewayJ
2026-10-08 16:53:17 +08:00
committed by GitHub
parent 89a277c3d9
commit bdd23da034
20 changed files with 930 additions and 309 deletions
+3 -17
View File
@@ -39,6 +39,7 @@ use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
use mime_guess::from_path;
use opentelemetry::global;
use rust_embed::RustEmbed;
use rustfs_iam::federation::FederatedProviderView;
use serde::Serialize;
use std::{
net::{IpAddr, SocketAddr},
@@ -185,13 +186,7 @@ pub(crate) struct Config {
license: License,
doc: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
oidc: Vec<OidcProviderInfo>,
}
#[derive(Debug, Serialize, Clone)]
struct OidcProviderInfo {
provider_id: String,
display_name: String,
oidc: Vec<FederatedProviderView>,
}
impl Config {
@@ -200,16 +195,7 @@ impl Config {
// Collect OIDC provider info if available
let oidc = current_federated_identity_service()
.map(|federation| {
federation
.list_visible_providers()
.into_iter()
.map(|p| OidcProviderInfo {
provider_id: p.provider_id,
display_name: p.display_name,
})
.collect()
})
.map(|federation| federation.list_visible_providers())
.unwrap_or_default();
Config {
+4 -2
View File
@@ -565,7 +565,8 @@ fn is_supported_idp_type(idp_type: &str) -> bool {
fn idp_config_names(idp_type: &str) -> S3Result<Vec<(String, bool)>> {
let config = current_active_server_config();
match idp_type {
IDP_TYPE_OPENID => Ok(rustfs_iam::oidc::load_effective_oidc_provider_configs(config.as_ref())
IDP_TYPE_OPENID => Ok(rustfs_iam::oidc::load_oidc_config_snapshot(config.as_ref())
.into_providers()
.into_iter()
.map(|provider| (provider.config.id, provider.config.enabled))
.collect()),
@@ -583,7 +584,8 @@ fn idp_config_info(idp_type: &str, name: &str) -> S3Result<(bool, Vec<IdpConfigE
let config = current_active_server_config();
match idp_type {
IDP_TYPE_OPENID => {
let provider = rustfs_iam::oidc::load_effective_oidc_provider_configs(config.as_ref())
let provider = rustfs_iam::oidc::load_oidc_config_snapshot(config.as_ref())
.into_providers()
.into_iter()
.find(|provider| provider.config.id == name || (name == DEFAULT_DELIMITER && provider.config.id == "default"))
.ok_or_else(|| s3_error!(NoSuchResource, "openid provider '{}' not found", name))?;
+39 -60
View File
@@ -325,7 +325,8 @@ impl Operation for GetOidcConfigHandler {
let config = load_server_config_from_store().await?;
let restart_required = oidc_restart_required(&config);
let providers = rustfs_iam::oidc::load_effective_oidc_provider_configs(Some(&config))
let providers = rustfs_iam::oidc::load_oidc_config_snapshot(Some(&config))
.into_providers()
.into_iter()
.map(|provider| OidcConfigView {
provider_id: provider.config.id.clone(),
@@ -697,45 +698,37 @@ impl Operation for OidcLogoutHandler {
/// from request headers. For production deployments behind a reverse proxy, configuring
/// an explicit redirect_uri is recommended to prevent header manipulation.
fn derive_callback_uri(req: &S3Request<Body>, provider_id: &str) -> S3Result<String> {
if let Some(federation) = current_federated_identity_service()
&& let Some(config) = federation.get_provider_config(provider_id)
{
return derive_callback_uri_with_provider_config(req, provider_id, Some(config));
}
let policy = current_federated_identity_service().and_then(|federation| federation.redirect_policy(provider_id));
derive_callback_uri_with_provider_config(req, provider_id, None)
derive_callback_uri_with_redirect_policy(req, provider_id, policy.as_ref())
}
fn derive_callback_uri_with_provider_config(
fn derive_callback_uri_with_redirect_policy(
req: &S3Request<Body>,
provider_id: &str,
config: Option<&rustfs_iam::oidc::OidcProviderConfig>,
policy: Option<&rustfs_iam::federation::FederatedRedirectPolicy>,
) -> S3Result<String> {
if let Some(config) = config {
if let Some(ref uri) = config.redirect_uri {
let parsed = Url::parse(uri).map_err(|_| s3_error!(InvalidRequest, "invalid configured redirect_uri"))?;
if !is_valid_scheme(parsed.scheme()) || parsed.host_str().is_none() {
return Err(s3_error!(InvalidRequest, "configured redirect_uri must be absolute http/https URL"));
}
return Ok(uri.clone());
}
if let Some(url) = browser_redirect_url(&oidc_callback_path(provider_id))? {
return Ok(url);
}
if !config.redirect_uri_dynamic {
return Err(s3_error!(
InvalidRequest,
"provider requires explicit redirect_uri because redirect_uri_dynamic is disabled"
));
if let Some(policy) = policy
&& let Some(ref uri) = policy.redirect_uri
{
let parsed = Url::parse(uri).map_err(|_| s3_error!(InvalidRequest, "invalid configured redirect_uri"))?;
if !is_valid_scheme(parsed.scheme()) || parsed.host_str().is_none() {
return Err(s3_error!(InvalidRequest, "configured redirect_uri must be absolute http/https URL"));
}
return Ok(uri.clone());
}
if let Some(url) = browser_redirect_url(&oidc_callback_path(provider_id))? {
return Ok(url);
}
if policy.is_some_and(|policy| !policy.allow_request_origin) {
return Err(s3_error!(
InvalidRequest,
"provider requires explicit redirect_uri because redirect_uri_dynamic is disabled"
));
}
let scheme = extract_request_scheme(req)?;
let host = extract_request_host(req)?;
@@ -938,8 +931,7 @@ fn oidc_restart_required(config: &ServerConfig) -> bool {
}
fn oidc_restart_required_from_active_config(config: &ServerConfig, active_config: Option<&ServerConfig>) -> bool {
rustfs_iam::oidc::load_effective_oidc_provider_configs(Some(config))
!= rustfs_iam::oidc::load_effective_oidc_provider_configs(active_config)
rustfs_iam::oidc::load_oidc_config_snapshot(Some(config)) != rustfs_iam::oidc::load_oidc_config_snapshot(active_config)
}
fn default_oidc_kvs() -> s3s::S3Result<rustfs_config::server_config::KVS> {
@@ -1419,27 +1411,13 @@ mod tests {
}
}
fn test_provider_config(redirect_uri: Option<&str>, redirect_uri_dynamic: bool) -> rustfs_iam::oidc::OidcProviderConfig {
rustfs_iam::oidc::OidcProviderConfig {
id: "default".to_string(),
enabled: true,
config_url: "https://idp.example.com/.well-known/openid-configuration".to_string(),
issuer: None,
client_id: "rustfs-console".to_string(),
client_secret: None,
scopes: vec!["openid".to_string()],
other_audiences: Vec::new(),
fn test_redirect_policy(
redirect_uri: Option<&str>,
allow_request_origin: bool,
) -> rustfs_iam::federation::FederatedRedirectPolicy {
rustfs_iam::federation::FederatedRedirectPolicy {
redirect_uri: redirect_uri.map(ToString::to_string),
redirect_uri_dynamic,
claim_name: OIDC_DEFAULT_CLAIM_NAME.to_string(),
claim_prefix: String::new(),
role_policy: String::new(),
display_name: "default".to_string(),
groups_claim: OIDC_DEFAULT_GROUPS_CLAIM.to_string(),
roles_claim: OIDC_DEFAULT_ROLES_CLAIM.to_string(),
email_claim: OIDC_DEFAULT_EMAIL_CLAIM.to_string(),
username_claim: OIDC_DEFAULT_USERNAME_CLAIM.to_string(),
hide_from_ui: false,
allow_request_origin,
}
}
@@ -1604,7 +1582,7 @@ mod tests {
}
#[test]
fn test_derive_callback_uri_falls_back_to_request_headers() {
fn test_derive_callback_uri_missing_provider_falls_back_to_request_headers() {
let req = build_oidc_request(
"http://internal/rustfs/admin/v3/oidc/authorize/default",
Some("internal:9000"),
@@ -1612,19 +1590,20 @@ mod tests {
);
let callback = with_var(ENV_RUSTFS_BROWSER_REDIRECT_URL, None::<&str>, || {
derive_callback_uri(&req, "default").expect("callback URI should fall back to request headers")
derive_callback_uri_with_redirect_policy(&req, "missing", None)
.expect("a missing provider should keep the request-header fallback")
});
assert_eq!(callback, "https://internal:9000/rustfs/admin/v3/oidc/callback/default");
assert_eq!(callback, "https://internal:9000/rustfs/admin/v3/oidc/callback/missing");
}
#[test]
fn test_derive_callback_uri_configured_redirect_uri_wins() {
let req = build_oidc_request("http://internal/rustfs/admin/v3/oidc/authorize/default", Some("internal:9000"), None);
let config = test_provider_config(Some("https://configured.example.com/rustfs/admin/v3/oidc/callback/default"), false);
let policy = test_redirect_policy(Some("https://configured.example.com/rustfs/admin/v3/oidc/callback/default"), false);
let callback = with_var(ENV_RUSTFS_BROWSER_REDIRECT_URL, Some("https://console.example.com"), || {
derive_callback_uri_with_provider_config(&req, "default", Some(&config))
derive_callback_uri_with_redirect_policy(&req, "default", Some(&policy))
.expect("configured redirect_uri should be preferred")
});
@@ -1634,10 +1613,10 @@ mod tests {
#[test]
fn test_derive_callback_uri_browser_redirect_url_satisfies_static_provider() {
let req = build_oidc_request("http://internal/rustfs/admin/v3/oidc/authorize/default", Some("internal:9000"), None);
let config = test_provider_config(None, false);
let policy = test_redirect_policy(None, false);
let callback = with_var(ENV_RUSTFS_BROWSER_REDIRECT_URL, Some("https://console.example.com"), || {
derive_callback_uri_with_provider_config(&req, "default", Some(&config))
derive_callback_uri_with_redirect_policy(&req, "default", Some(&policy))
.expect("browser redirect URL should satisfy a non-dynamic provider")
});
@@ -1647,10 +1626,10 @@ mod tests {
#[test]
fn test_derive_callback_uri_static_provider_requires_redirect_source() {
let req = build_oidc_request("http://internal/rustfs/admin/v3/oidc/authorize/default", Some("internal:9000"), None);
let config = test_provider_config(None, false);
let policy = test_redirect_policy(None, false);
let err = with_var(ENV_RUSTFS_BROWSER_REDIRECT_URL, None::<&str>, || {
derive_callback_uri_with_provider_config(&req, "default", Some(&config))
derive_callback_uri_with_redirect_policy(&req, "default", Some(&policy))
.expect_err("non-dynamic provider without redirect source should fail")
});
@@ -1664,10 +1643,10 @@ mod tests {
Some("internal:9000"),
Some("https"),
);
let config = test_provider_config(None, true);
let policy = test_redirect_policy(None, true);
let callback = with_var(ENV_RUSTFS_BROWSER_REDIRECT_URL, None::<&str>, || {
derive_callback_uri_with_provider_config(&req, "default", Some(&config))
derive_callback_uri_with_redirect_policy(&req, "default", Some(&policy))
.expect("dynamic provider should fall back to request headers")
});
+263 -36
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_service, current_iam_handle, current_object_store_handle, current_region,
current_deployment_id, current_federated_identity_runtime, current_iam_handle, current_object_store_handle, current_region,
current_replication_pool_handle, current_replication_stats_handle, current_server_config, current_token_signing_key,
object_store_from_req,
};
@@ -2032,8 +2032,8 @@ async fn bootstrap_existing_metadata_after_add(
}
/// The OpenID half of the IDP settings a site reports to its site-replication
/// peers, built from the providers this site has configured (in
/// `list_providers` order) and the site's own region.
/// peers, built from the active OIDC configuration snapshot and the site's own
/// region.
fn open_id_settings(providers: Vec<(String, OpenIDProviderSettings)>, region: String) -> OpenIDSettings {
// `region` qualifies the provider identities, not the site: a site
// without OpenID must report the empty settings in every region, or the
@@ -2043,53 +2043,84 @@ fn open_id_settings(providers: Vec<(String, OpenIDProviderSettings)>, region: St
return OpenIDSettings::default();
}
let mut providers = providers;
let claim_provider_index = providers
.iter()
.position(|(provider_id, _)| provider_id == "default")
.unwrap_or_default();
let (_, claim_provider) = providers.remove(claim_provider_index);
let mut settings = OpenIDSettings {
enabled: true,
region,
claim_provider,
..Default::default()
};
for (provider_id, provider_settings) in providers {
let claim_provider_unset = settings.claim_provider.client_id.is_empty()
&& settings.claim_provider.claim_name.is_empty()
&& settings.claim_provider.role_policy.is_empty()
&& settings.claim_provider.hashed_client_secret.is_empty();
if provider_id == "default" || claim_provider_unset {
settings.claim_provider = provider_settings;
} else {
settings.roles.insert(provider_id, provider_settings);
}
settings.roles.insert(provider_id, provider_settings);
}
settings
}
fn open_id_settings_from_snapshot(snapshot: &rustfs_iam::oidc::OidcConfigSnapshot, region: String) -> OpenIDSettings {
let providers = snapshot
.providers()
.iter()
.map(|provider| {
let config = &provider.config;
(config.id.clone(), open_id_provider_settings(config))
})
.collect();
open_id_settings(providers, region)
}
fn open_id_provider_settings(config: &rustfs_iam::oidc::OidcProviderConfig) -> OpenIDProviderSettings {
OpenIDProviderSettings {
claim_name: config.claim_name.clone(),
claim_userinfo_enabled: false,
role_policy: config.role_policy.clone(),
client_id: config.client_id.clone(),
hashed_client_secret: hash_client_secret(config.client_secret.as_deref()),
}
}
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();
if let Some(federation) = current_federated_identity_service() {
// A listed provider whose config cannot be resolved contributes
// nothing to the peer comparison, so it is dropped here rather than
// inside the settings builder.
let providers = federation
.list_providers()
.into_iter()
.filter_map(|provider| {
let config = federation.get_provider_config(&provider.provider_id)?;
Some((
provider.provider_id.clone(),
OpenIDProviderSettings {
claim_name: config.claim_name.clone(),
claim_userinfo_enabled: false,
role_policy: config.role_policy.clone(),
client_id: config.client_id.clone(),
hashed_client_secret: hash_client_secret(config.client_secret.as_deref()),
},
))
})
.collect();
settings.open_id = open_id_settings(providers, current_region().map(|region| region.to_string()).unwrap_or_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)
})
.unwrap_or_default();
let (ldap, ldap_configs) = load_ldap_idp_settings();
settings.ldap = ldap;
@@ -11386,6 +11417,109 @@ mod tests {
}
}
fn oidc_snapshot_provider(id: &str, client_id: &str) -> rustfs_iam::oidc::SourcedOidcProviderConfig {
rustfs_iam::oidc::SourcedOidcProviderConfig {
config: rustfs_iam::oidc::OidcProviderConfig {
id: id.to_string(),
enabled: true,
config_url: format!("https://idp.example.com/{id}/.well-known/openid-configuration"),
issuer: None,
client_id: client_id.to_string(),
client_secret: Some("secret".to_string()),
scopes: vec!["openid".to_string()],
other_audiences: Vec::new(),
redirect_uri: None,
redirect_uri_dynamic: true,
claim_name: "groups".to_string(),
claim_prefix: String::new(),
role_policy: "readwrite".to_string(),
display_name: id.to_string(),
groups_claim: "groups".to_string(),
roles_claim: String::new(),
email_claim: "email".to_string(),
username_claim: "username".to_string(),
hide_from_ui: false,
},
source: rustfs_iam::oidc::OidcProviderConfigSource::Persisted,
}
}
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 {
@@ -11442,6 +11576,20 @@ mod tests {
assert_eq!(settings.claim_provider.client_id, "client-a");
}
#[test]
fn test_open_id_settings_uses_first_named_provider_without_default() {
let settings = open_id_settings(
vec![
("alpha".to_string(), openid_provider("client-a")),
("corp".to_string(), openid_provider("client-b")),
],
"eu-site-1".to_string(),
);
assert_eq!(settings.claim_provider.client_id, "client-a");
assert_eq!(settings.roles.get("corp").map(|provider| provider.client_id.as_str()), Some("client-b"));
}
#[test]
fn idp_settings_preserve_site_replication_wire_shape() {
let (ldap, ldap_configs) = ldap_settings_from_kvs(&ldap_settings_kvs());
@@ -11502,6 +11650,85 @@ mod tests {
);
}
#[test]
fn oidc_config_snapshot_preserves_site_replication_settings() {
let snapshot = rustfs_iam::oidc::OidcConfigSnapshot::new(vec![
oidc_snapshot_provider("corp", "client-b"),
oidc_snapshot_provider("default", "client-a"),
]);
assert_eq!(
snapshot
.providers()
.iter()
.map(|provider| provider.config.id.as_str())
.collect::<Vec<_>>(),
["default", "corp"]
);
assert_eq!(
serde_json::to_value(open_id_settings_from_snapshot(&snapshot, "eu-site-1".to_string()))
.expect("serialize OpenID settings"),
serde_json::json!({
"Enabled": true,
"Region": "eu-site-1",
"Roles": {
"corp": {
"ClaimName": "groups",
"ClaimUserinfoEnabled": false,
"RolePolicy": "readwrite",
"ClientID": "client-b",
"HashedClientSecret": "K7gNU3sdo-OL0wNhqoVWhr3g6s1xYv72ol_pe_Unols",
}
},
"ClaimProvider": {
"ClaimName": "groups",
"ClaimUserinfoEnabled": false,
"RolePolicy": "readwrite",
"ClientID": "client-a",
"HashedClientSecret": "K7gNU3sdo-OL0wNhqoVWhr3g6s1xYv72ol_pe_Unols",
}
})
);
}
#[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]
+6 -5
View File
@@ -23,11 +23,12 @@ 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_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_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,
};
#[cfg(test)]
pub(crate) use crate::runtime_sources::{
+111 -19
View File
@@ -38,7 +38,12 @@ use crate::app::object_data_cache::ObjectDataCacheAdapter;
use crate::config::RustFSBufferConfig;
use rustfs_config::server_config::Config;
use rustfs_credentials::Credentials;
use rustfs_iam::{error::Error as IamError, federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
use rustfs_iam::{
error::Error as IamError,
federation::{FederatedIdentityService, oidc::OidcConfigQuery},
store::object::ObjectStore,
sys::IamSys,
};
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
use rustfs_kms::{KmsServiceManager, ObjectEncryptionService};
use rustfs_lock::LockClient;
@@ -93,7 +98,25 @@ pub fn resolve_federated_identity_service() -> Option<Arc<FederatedIdentityServi
resolve_federated_identity_service_with(get_global_app_context())
}
/// Publish the initialized federated identity service into the global AppContext.
/// Resolve the OIDC configuration query using AppContext-first precedence.
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>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
) -> bool {
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)
}
@@ -333,9 +356,21 @@ fn resolve_federated_identity_service_with(context: Option<Arc<AppContext>>) ->
context.and_then(|context| context.federated_identity().handle())
}
fn resolve_oidc_config_query_with(context: Option<Arc<AppContext>>) -> Option<Arc<dyn OidcConfigQuery>> {
context.and_then(|context| context.federated_identity().oidc_config_query())
}
fn resolve_federated_identity_runtime_with(context: Option<Arc<AppContext>>) -> Option<FederatedIdentityRuntimeSnapshot> {
context.and_then(|context| context.federated_identity().runtime_snapshot())
}
#[cfg(test)]
fn publish_federated_identity_service_with(context: Option<Arc<AppContext>>, service: Arc<FederatedIdentityService>) -> bool {
context.is_some_and(|context| context.publish_federated_identity_service(service))
fn publish_federated_identity_runtime_with(
context: Option<Arc<AppContext>>,
service: Arc<FederatedIdentityService>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
) -> bool {
context.is_some_and(|context| context.publish_federated_identity_runtime(service, oidc_config_query))
}
fn resolve_ready_iam_handle_with(context: Arc<AppContext>) -> rustfs_iam::error::Result<Arc<IamSys<ObjectStore>>> {
@@ -518,7 +553,10 @@ mod tests {
use crate::config::{RustFSBufferConfig, WorkloadProfile};
use async_trait::async_trait;
use rustfs_iam::{
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
federation::{
FederatedIdentityRegistry, FederatedIdentityService,
oidc::{OidcConfigQuery, StandardOidcAdapter},
},
oidc::OidcSys,
store::object::ObjectStore,
sys::IamSys,
@@ -559,34 +597,65 @@ mod tests {
}
struct TestFederatedIdentityInterface {
service: StdRwLock<Option<Arc<FederatedIdentityService>>>,
runtime: StdRwLock<Option<TestFederatedIdentityRuntime>>,
}
struct TestFederatedIdentityRuntime {
service: Arc<FederatedIdentityService>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
}
impl TestFederatedIdentityInterface {
fn new(service: Option<Arc<FederatedIdentityService>>) -> Self {
fn new(runtime: Option<(Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>)>) -> Self {
Self {
service: StdRwLock::new(service),
runtime: StdRwLock::new(runtime.map(|(service, oidc_config_query)| TestFederatedIdentityRuntime {
service,
oidc_config_query,
})),
}
}
}
impl FederatedIdentityInterface for TestFederatedIdentityInterface {
fn handle(&self) -> Option<Arc<FederatedIdentityService>> {
self.service.read().ok().and_then(|service| service.as_ref().cloned())
self.runtime
.read()
.ok()
.and_then(|runtime| runtime.as_ref().map(|runtime| Arc::clone(&runtime.service)))
}
fn publish_handle(&self, service: Arc<FederatedIdentityService>) -> bool {
let Ok(mut published_service) = self.service.write() else {
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))))
})
}
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_service = Some(service);
*published_runtime = Some(TestFederatedIdentityRuntime {
service,
oidc_config_query,
});
true
}
}
fn test_federated_identity_service(oidc: OidcSys) -> Arc<FederatedIdentityService> {
fn test_federated_identity_runtime(oidc: OidcSys) -> (Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>) {
let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc)));
Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter)))
let oidc_config_query = adapter.clone();
let service = Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter)));
(service, oidc_config_query)
}
struct TestKmsInterface {
@@ -1012,8 +1081,8 @@ mod tests {
Ok(sys) => sys,
Err(err) => unreachable!("test OIDC fallback sys should initialize: {err}"),
};
let context_oidc = test_federated_identity_service(context_oidc_sys);
let fallback_oidc = test_federated_identity_service(fallback_oidc_sys);
let (context_oidc, context_oidc_query) = test_federated_identity_runtime(context_oidc_sys);
let (fallback_oidc, fallback_oidc_query) = test_federated_identity_runtime(fallback_oidc_sys);
let context_token_signing_key = "context-token-signing-key".to_string();
let context = Arc::new(AppContext::with_test_interfaces(
@@ -1124,12 +1193,33 @@ mod tests {
);
assert!(resolve_iam_ready_with(Some(context.clone())).expect("context IAM ready"));
assert!(resolve_federated_identity_service_with(Some(context.clone())).is_none());
assert!(publish_federated_identity_service_with(Some(context.clone()), context_oidc.clone()));
assert!(resolve_oidc_config_query_with(Some(context.clone())).is_none());
assert!(publish_federated_identity_runtime_with(
Some(context.clone()),
context_oidc.clone(),
context_oidc_query.clone(),
));
let resolved_oidc = resolve_federated_identity_service_with(Some(context.clone()));
assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &context_oidc)));
assert!(publish_federated_identity_service_with(Some(context.clone()), fallback_oidc.clone()));
let resolved_oidc_query = resolve_oidc_config_query_with(Some(context.clone())).expect("context OIDC config query");
assert!(Arc::ptr_eq(&resolved_oidc_query, &context_oidc_query));
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!(publish_federated_identity_runtime_with(
Some(context.clone()),
fallback_oidc.clone(),
fallback_oidc_query.clone(),
));
let resolved_oidc = resolve_federated_identity_service_with(Some(context.clone()));
assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &fallback_oidc)));
let resolved_oidc_query = resolve_oidc_config_query_with(Some(context.clone())).expect("fallback OIDC config query");
assert!(Arc::ptr_eq(&resolved_oidc_query, &fallback_oidc_query));
assert_eq!(
resolve_token_signing_key_with(Some(context.clone())).as_deref(),
Some(context_token_signing_key.as_str())
@@ -1250,9 +1340,11 @@ mod tests {
assert!(resolve_iam_ready_with(None).is_none());
assert!(resolve_iam_handle_with(None).is_none());
assert!(resolve_federated_identity_service_with(None).is_none());
assert!(resolve_oidc_config_query_with(None).is_none());
assert!(resolve_federated_identity_runtime_with(None).is_none());
assert!(resolve_token_signing_key_with(None).is_none());
assert!(resolve_notify_interface_for_context(None).is_none());
assert!(!publish_federated_identity_service_with(None, context_oidc));
assert!(!publish_federated_identity_runtime_with(None, context_oidc, context_oidc_query));
assert!(resolve_bucket_metadata_handle_with(None).is_none());
assert!(resolve_bucket_monitor_handle_with(None).is_none());
assert!(resolve_replication_pool_handle_with(None).is_none());
+14 -1
View File
@@ -35,7 +35,11 @@ use super::interfaces::{
};
use crate::app::object_data_cache::ObjectDataCacheAdapter;
use crate::app::object_traffic_health::ObjectTrafficHealth;
use rustfs_iam::{federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
use rustfs_iam::{
federation::{FederatedIdentityService, oidc::OidcConfigQuery},
store::object::ObjectStore,
sys::IamSys,
};
use rustfs_kms::KmsServiceManager;
use std::sync::{Arc, OnceLock};
@@ -157,6 +161,15 @@ impl AppContext {
self.federated_identity.clone()
}
pub fn publish_federated_identity_runtime(
&self,
service: Arc<FederatedIdentityService>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
) -> bool {
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)
}
+128 -24
View File
@@ -20,18 +20,23 @@ use super::super::storage_api::context::runtime::{
};
use super::interfaces::{
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BucketMonitorInterface, BufferConfigInterface,
DeploymentIdInterface, EndpointsInterface, ExpiryStateInterface, FederatedIdentityInterface, IamInterface,
InternodeMetricsInterface, KmsInterface, KmsRuntimeInterface, LocalNodeNameInterface, LockClientInterface,
LockClientsInterface, NotificationSystemInterface, NotifyInterface, OutboundTlsRuntimeInterface, PerformanceMetricsInterface,
RegionInterface, ReplicationPoolInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface,
ScannerMetricsInterface, ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
DeploymentIdInterface, EndpointsInterface, ExpiryStateInterface, FederatedIdentityInterface,
FederatedIdentityRuntimeSnapshot, IamInterface, InternodeMetricsInterface, KmsInterface, KmsRuntimeInterface,
LocalNodeNameInterface, LockClientInterface, LockClientsInterface, NotificationSystemInterface, NotifyInterface,
OutboundTlsRuntimeInterface, PerformanceMetricsInterface, RegionInterface, ReplicationPoolInterface,
ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface, ScannerMetricsInterface, ServerConfigInterface,
StorageClassInterface, TierConfigInterface, TransitionStateInterface,
};
use super::runtime_sources;
use crate::config::RustFSBufferConfig;
use async_trait::async_trait;
use rustfs_config::server_config::Config;
use rustfs_credentials::Credentials;
use rustfs_iam::{federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
use rustfs_iam::{
federation::{FederatedIdentityService, oidc::OidcConfigQuery},
store::object::ObjectStore,
sys::IamSys,
};
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
use rustfs_kms::KmsServiceManager;
use rustfs_lock::LockClient;
@@ -75,15 +80,32 @@ impl IamInterface for IamHandle {
}
}
/// Default federated identity service interface adapter.
/// Default federation runtime interface adapter.
pub struct FederatedIdentityHandle {
service: StdRwLock<Option<Arc<FederatedIdentityService>>>,
runtime: StdRwLock<Option<FederatedIdentityRuntime>>,
}
struct FederatedIdentityRuntime {
service: Arc<FederatedIdentityService>,
oidc_config_query: Option<Arc<dyn OidcConfigQuery>>,
}
impl FederatedIdentityHandle {
pub fn new(service: Option<Arc<FederatedIdentityService>>) -> Self {
Self {
service: StdRwLock::new(service),
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),
})),
}
}
}
@@ -96,14 +118,40 @@ impl Default for FederatedIdentityHandle {
impl FederatedIdentityInterface for FederatedIdentityHandle {
fn handle(&self) -> Option<Arc<FederatedIdentityService>> {
self.service.read().ok().and_then(|service| service.as_ref().cloned())
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)))
})
}
fn publish_handle(&self, service: Arc<FederatedIdentityService>) -> bool {
let Ok(mut published_service) = self.service.write() else {
let Ok(mut published_runtime) = self.runtime.write() else {
return false;
};
*published_service = Some(service);
*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),
});
true
}
}
@@ -584,6 +632,14 @@ pub fn default_federated_identity_interface() -> Arc<dyn FederatedIdentityInterf
default_federated_identity_handle()
}
pub fn publish_default_federated_identity_runtime(
service: Arc<FederatedIdentityService>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
) -> bool {
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)
}
@@ -592,6 +648,12 @@ pub fn federated_identity_interface(service: Option<Arc<FederatedIdentityService
Arc::new(FederatedIdentityHandle::new(service))
}
pub fn federated_identity_runtime_interface(
runtime: Option<(Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>)>,
) -> Arc<dyn FederatedIdentityInterface> {
Arc::new(FederatedIdentityHandle::with_runtime(runtime))
}
pub fn default_region_interface() -> Arc<dyn RegionInterface> {
Arc::new(RegionHandle)
}
@@ -624,28 +686,34 @@ pub fn default_buffer_config_interface() -> Arc<dyn BufferConfigInterface> {
mod tests {
use super::{
KmsRuntimeHandle, KmsServiceManager, ServerConfigHandle, default_federated_identity_interface,
federated_identity_interface, publish_default_federated_identity_service, runtime_sources,
federated_identity_interface, 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, oidc::StandardOidcAdapter},
federation::{
FederatedIdentityRegistry, FederatedIdentityService,
oidc::{OidcConfigQuery, StandardOidcAdapter},
},
oidc::OidcSys,
};
use std::collections::HashMap;
use std::sync::Arc;
fn test_federated_identity_service() -> Arc<FederatedIdentityService> {
fn test_federated_identity_runtime() -> (Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>) {
let oidc = OidcSys::empty().expect("empty OIDC configuration should be valid");
let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc)));
Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter)))
let oidc_config_query = adapter.clone();
let service = Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter)));
(service, oidc_config_query)
}
#[test]
fn federated_identity_handle_preserves_early_publish_and_allows_replacement() {
let first = test_federated_identity_service();
let second = test_federated_identity_service();
let interface = federated_identity_interface(Some(first.clone()));
let (first, first_query) = test_federated_identity_runtime();
let (second, second_query) = test_federated_identity_runtime();
let interface = federated_identity_runtime_interface(Some((first.clone(), first_query.clone())));
assert!(
interface
@@ -654,21 +722,57 @@ mod tests {
.is_some_and(|resolved| Arc::ptr_eq(resolved, &first))
);
assert!(interface.publish_handle(second.clone()));
let resolved_query = interface.oidc_config_query().expect("initial OIDC config query");
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!(interface.publish_runtime(second.clone(), second_query.clone()));
assert!(
interface
.handle()
.as_ref()
.is_some_and(|resolved| Arc::ptr_eq(resolved, &second))
);
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());
}
#[test]
fn default_federated_identity_handle_shares_early_publish_and_replacement() {
let first = test_federated_identity_service();
let second = test_federated_identity_service();
let (first, first_query) = test_federated_identity_runtime();
let (second, second_query) = test_federated_identity_runtime();
assert!(publish_default_federated_identity_service(first.clone()));
assert!(publish_default_federated_identity_runtime(first.clone(), first_query));
let interface = default_federated_identity_interface();
assert!(
interface
@@ -677,7 +781,7 @@ mod tests {
.is_some_and(|resolved| Arc::ptr_eq(resolved, &first))
);
assert!(publish_default_federated_identity_service(second.clone()));
assert!(publish_default_federated_identity_runtime(second.clone(), second_query));
assert!(
interface
.handle()
+20 -2
View File
@@ -22,7 +22,11 @@ use crate::config::RustFSBufferConfig;
use async_trait::async_trait;
use rustfs_config::server_config::Config;
use rustfs_credentials::Credentials;
use rustfs_iam::{federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
use rustfs_iam::{
federation::{FederatedIdentityService, oidc::OidcConfigQuery},
store::object::ObjectStore,
sys::IamSys,
};
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
use rustfs_kms::KmsServiceManager;
use rustfs_lock::LockClient;
@@ -43,12 +47,26 @@ pub trait IamInterface: Send + Sync {
}
}
/// Federated identity service interface for admin and runtime consumers.
/// Published federation runtime handles for authentication and OIDC configuration consumers.
pub type FederatedIdentityRuntimeSnapshot = (Arc<FederatedIdentityService>, Option<Arc<dyn OidcConfigQuery>>);
pub trait FederatedIdentityInterface: Send + Sync {
fn handle(&self) -> Option<Arc<FederatedIdentityService>>;
fn oidc_config_query(&self) -> Option<Arc<dyn OidcConfigQuery>> {
None
}
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 {
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 -16
View File
@@ -14,7 +14,7 @@
use crate::app::context;
use rustfs_config::ENV_RUSTFS_BROWSER_REDIRECT_URL;
use rustfs_iam::federation::FederatedIdentityService;
use rustfs_iam::federation::{FederatedIdentityService, oidc::OidcConfigQuery};
use std::sync::Arc;
use tracing::warn;
@@ -35,6 +35,7 @@ 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,
@@ -58,7 +59,15 @@ pub(crate) use context::{
resolve_tier_config_handle as current_tier_config_handle, resolve_token_signing_key as current_token_signing_key,
};
pub(crate) fn publish_federated_identity_service(service: Arc<FederatedIdentityService>) -> bool {
pub(crate) fn publish_federated_identity_runtime(
service: Arc<FederatedIdentityService>,
oidc_config_query: Arc<dyn OidcConfigQuery>,
) -> bool {
warn_on_browser_redirect_fallback(&service);
context::publish_federated_identity_runtime(service, oidc_config_query)
}
fn warn_on_browser_redirect_fallback(service: &FederatedIdentityService) {
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()) {
warn!(
@@ -72,8 +81,6 @@ pub(crate) fn publish_federated_identity_service(service: Arc<FederatedIdentityS
"OIDC browser redirect fallback enabled"
);
}
context::publish_federated_identity_service(service)
}
#[cfg(test)]
@@ -96,13 +103,12 @@ pub(crate) fn current_app_context() -> Option<Arc<AppContext>> {
#[cfg(test)]
mod tests {
use super::*;
use rustfs_iam::{
federation::{
FederatedAuthorization, FederatedCodeExchange, FederatedIdentityProvider, FederatedIdentityRegistry,
Result as FederationResult,
},
oidc::{OidcProviderConfig, OidcProviderSummary},
use rustfs_iam::federation::oidc::OidcConfigQuery;
use rustfs_iam::federation::{
FederatedAuthorization, FederatedCodeExchange, FederatedIdentityProvider, FederatedIdentityRegistry,
FederatedProviderView, FederatedRedirectPolicy, Result as FederationResult,
};
use rustfs_iam::oidc::OidcConfigSnapshot;
use std::{
io::{self, Write},
sync::Mutex,
@@ -119,15 +125,15 @@ mod tests {
self.has_providers
}
fn list_providers(&self) -> Vec<OidcProviderSummary> {
fn list_providers(&self) -> Vec<FederatedProviderView> {
Vec::new()
}
fn list_visible_providers(&self) -> Vec<OidcProviderSummary> {
fn list_visible_providers(&self) -> Vec<FederatedProviderView> {
Vec::new()
}
fn provider_config(&self, _id: &str) -> Option<&OidcProviderConfig> {
fn redirect_policy(&self, _provider_id: &str) -> Option<FederatedRedirectPolicy> {
None
}
@@ -161,6 +167,12 @@ mod tests {
}
}
impl OidcConfigQuery for TestProvider {
fn config_snapshot(&self) -> OidcConfigSnapshot {
OidcConfigSnapshot::new(Vec::new())
}
}
#[derive(Clone, Default)]
struct CapturedLogs(Arc<Mutex<Vec<u8>>>);
@@ -185,9 +197,10 @@ mod tests {
}
}
fn test_service(has_providers: bool) -> Arc<FederatedIdentityService> {
fn test_runtime(has_providers: bool) -> (Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>) {
let provider = Arc::new(TestProvider { has_providers });
Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(provider)))
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 {
@@ -204,7 +217,8 @@ mod tests {
);
tracing::subscriber::with_default(subscriber, || {
assert!(publish_federated_identity_service(test_service(has_providers)));
let (service, oidc_config_query) = test_runtime(has_providers);
assert!(publish_federated_identity_runtime(service, oidc_config_query));
});
String::from_utf8(captured.lock().expect("captured log lock").clone()).expect("captured logs must be UTF-8")
+15 -8
View File
@@ -13,7 +13,10 @@
// limitations under the License.
use rustfs_iam::{
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
federation::{
FederatedIdentityRegistry, FederatedIdentityService,
oidc::{OidcConfigQuery, StandardOidcAdapter},
},
get_oidc, init_oidc_sys_with_extra_root_ca_provider,
oidc::{OidcExtraRootCaMaterial, OidcExtraRootCaProvider, OidcSys},
};
@@ -56,7 +59,8 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
match init_oidc_sys_with_extra_root_ca_provider(oidc_extra_root_ca_provider()).await {
Ok(()) => {
if let Some(oidc) = get_oidc() {
crate::runtime_sources::publish_federated_identity_service(standard_oidc_service(oidc));
let (service, oidc_config_query) = standard_oidc_runtime(oidc);
crate::runtime_sources::publish_federated_identity_runtime(service, oidc_config_query);
}
}
Err(e) => {
@@ -73,10 +77,11 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
Ok(())
}
fn standard_oidc_service(oidc: Arc<OidcSys>) -> Arc<FederatedIdentityService> {
fn standard_oidc_runtime(oidc: Arc<OidcSys>) -> (Arc<FederatedIdentityService>, Arc<dyn OidcConfigQuery>) {
let adapter = Arc::new(StandardOidcAdapter::new(oidc));
let oidc_config_query = adapter.clone();
let registry = FederatedIdentityRegistry::new(adapter);
Arc::new(FederatedIdentityService::new(registry))
(Arc::new(FederatedIdentityService::new(registry)), oidc_config_query)
}
pub(crate) fn oidc_extra_root_ca_provider() -> OidcExtraRootCaProvider {
@@ -121,14 +126,16 @@ mod tests {
use super::*;
#[test]
fn standard_oidc_service_retains_one_oidc_arc() {
fn standard_oidc_runtime_retains_one_oidc_arc() {
let oidc = Arc::new(OidcSys::empty().expect("empty OIDC configuration should be valid"));
let retained = Arc::clone(&oidc);
let service = standard_oidc_service(oidc);
let (service, oidc_config_query) = standard_oidc_runtime(oidc);
assert_eq!(Arc::strong_count(&retained), 2, "the service should retain one OIDC Arc");
assert_eq!(Arc::strong_count(&retained), 2, "the runtime should retain one OIDC Arc");
drop(service);
assert_eq!(Arc::strong_count(&retained), 1, "dropping the service should release its OIDC Arc");
assert_eq!(Arc::strong_count(&retained), 2, "the query should retain the shared adapter");
drop(oidc_config_query);
assert_eq!(Arc::strong_count(&retained), 1, "dropping the runtime should release its OIDC Arc");
}
}