mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-13 16:11:56 +00:00
refactor(iam): separate federated provider views (#8415)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user