feat(oidc): add HIDE_FROM_UI option to exclude providers from console login (#3162)

Add `RUSTFS_IDENTITY_OPENID_HIDE_FROM_UI[_<SUFFIX>]` setting that
removes a provider from the login page while keeping it fully
functional for STS AssumeRoleWithWebIdentity and site-replication.

Changes:
- Add `hide_from_ui: bool` to `OidcProviderConfig`
- Add `list_visible_providers()` that filters hidden providers
  (used by console login and /v3/oidc/providers endpoint)
- Keep `list_providers()` unfiltered for site-replication/admin config
- Extract `normalize_provider_config(config) -> config` to deduplicate
  field normalization (accepts the struct directly, not 18 parameters)
- Add `parse_enable_state()` helper for consistent EnableState parsing
- Plumb through admin API request structs (`#[serde(default)]`)
- Expose in `OidcConfigView` for admin GET config round-trip
- Persist via `upsert_persisted_provider_config()`

Note: adding `hide_from_ui` to the public `OidcProviderConfig` struct
is a source-level change for code constructing it with struct literals.
This is acceptable for the current pre-1.0 release cycle.

Signed-off-by: Alexander Kharkevich <alex@mara.com>
Co-authored-by: GatewayJ <835269233@qq.com>
This commit is contained in:
Alexander Kharkevich
2026-06-03 09:37:44 -04:00
committed by GitHub
parent be98c1f86a
commit ce6fcf39b1
4 changed files with 352 additions and 106 deletions
+5 -1
View File
@@ -28,6 +28,7 @@ pub const OIDC_GROUPS_CLAIM: &str = "groups_claim";
pub const OIDC_ROLES_CLAIM: &str = "roles_claim";
pub const OIDC_EMAIL_CLAIM: &str = "email_claim";
pub const OIDC_USERNAME_CLAIM: &str = "username_claim";
pub const OIDC_HIDE_FROM_UI: &str = "hide_from_ui";
// Environment variable names for OIDC
pub const ENV_IDENTITY_OPENID_ENABLE: &str = "RUSTFS_IDENTITY_OPENID_ENABLE";
@@ -46,9 +47,10 @@ pub const ENV_IDENTITY_OPENID_GROUPS_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_GROUP
pub const ENV_IDENTITY_OPENID_ROLES_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_ROLES_CLAIM";
pub const ENV_IDENTITY_OPENID_EMAIL_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_EMAIL_CLAIM";
pub const ENV_IDENTITY_OPENID_USERNAME_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_USERNAME_CLAIM";
pub const ENV_IDENTITY_OPENID_HIDE_FROM_UI: &str = "RUSTFS_IDENTITY_OPENID_HIDE_FROM_UI";
/// List of all environment variable keys for an OIDC provider.
pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 16] = &[
pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 17] = &[
ENV_IDENTITY_OPENID_ENABLE,
ENV_IDENTITY_OPENID_CONFIG_URL,
ENV_IDENTITY_OPENID_CLIENT_ID,
@@ -65,6 +67,7 @@ pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 16] = &[
ENV_IDENTITY_OPENID_ROLES_CLAIM,
ENV_IDENTITY_OPENID_EMAIL_CLAIM,
ENV_IDENTITY_OPENID_USERNAME_CLAIM,
ENV_IDENTITY_OPENID_HIDE_FROM_UI,
];
/// A list of all valid configuration keys for an OIDC provider.
@@ -85,6 +88,7 @@ pub const IDENTITY_OPENID_KEYS: &[&str] = &[
OIDC_ROLES_CLAIM,
OIDC_EMAIL_CLAIM,
OIDC_USERNAME_CLAIM,
OIDC_HIDE_FROM_UI,
crate::COMMENT_KEY,
];
+227 -24
View File
@@ -295,6 +295,7 @@ pub struct OidcProviderConfig {
pub roles_claim: String,
pub email_claim: String,
pub username_claim: String,
pub hide_from_ui: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
@@ -422,7 +423,7 @@ impl OidcSys {
!self.configs.is_empty()
}
/// Return provider summaries for the console UI.
/// List all providers (including hidden ones). Used by site-replication and admin config.
pub fn list_providers(&self) -> Vec<OidcProviderSummary> {
self.configs
.values()
@@ -433,6 +434,18 @@ impl OidcSys {
.collect()
}
/// List only visible providers (excludes those with `hide_from_ui = true`).
pub fn list_visible_providers(&self) -> Vec<OidcProviderSummary> {
self.configs
.values()
.filter(|c| !c.hide_from_ui)
.map(|c| OidcProviderSummary {
provider_id: c.id.clone(),
display_name: c.display_name.clone(),
})
.collect()
}
/// Build the PKCE authorization URL for a provider, store state in the state store.
pub async fn authorize_url(
&self,
@@ -918,6 +931,19 @@ impl OidcSys {
configs
}
/// Parse a string as an `EnableState` boolean.
/// Returns `default_if_empty` when the input is empty, and `default_on_error`
/// when parsing fails.
fn parse_enable_state(value: &str, default_if_empty: bool, default_on_error: bool) -> bool {
if value.is_empty() {
return default_if_empty;
}
value
.parse::<EnableState>()
.map(|s| s.is_enabled())
.unwrap_or(default_on_error)
}
/// Parse a single provider's config from env vars with the given suffix.
fn parse_single_provider(env_suffix: &str, id: &str) -> Option<OidcProviderConfig> {
let get_env = |base: &str| -> String { std::env::var(format!("{base}{env_suffix}")).unwrap_or_default() };
@@ -930,11 +956,7 @@ impl OidcSys {
return None;
}
let enabled = enable_val.is_empty()
|| enable_val
.parse::<rustfs_config::EnableState>()
.map(|s| s.is_enabled())
.unwrap_or(false);
let enabled = Self::parse_enable_state(&enable_val, true, false);
let scopes_str = get_env(ENV_IDENTITY_OPENID_SCOPES);
let scopes = if scopes_str.is_empty() {
@@ -951,12 +973,7 @@ impl OidcSys {
.map(|s| s.to_string())
.collect();
let redirect_uri_dynamic_str = get_env(ENV_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC);
let redirect_uri_dynamic = redirect_uri_dynamic_str.is_empty()
|| redirect_uri_dynamic_str
.parse::<rustfs_config::EnableState>()
.map(|s| s.is_enabled())
.unwrap_or(true);
let redirect_uri_dynamic = Self::parse_enable_state(&get_env(ENV_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC), true, true);
let claim_name = {
let v = get_env(ENV_IDENTITY_OPENID_CLAIM_NAME);
@@ -1003,6 +1020,7 @@ impl OidcSys {
let v = get_env(ENV_IDENTITY_OPENID_CLIENT_SECRET);
if v.is_empty() { None } else { Some(v) }
};
let hide_from_ui = Self::parse_enable_state(&get_env(ENV_IDENTITY_OPENID_HIDE_FROM_UI), false, false);
Some(OidcProviderConfig {
id: id.to_string(),
@@ -1022,6 +1040,7 @@ impl OidcSys {
roles_claim,
email_claim,
username_claim,
hide_from_ui,
})
}
@@ -1031,12 +1050,7 @@ impl OidcSys {
return None;
}
let enabled = kvs
.lookup(ENABLE_KEY)
.unwrap_or_else(|| EnableState::Off.to_string())
.parse::<EnableState>()
.map(|s| s.is_enabled())
.unwrap_or(false);
let enabled = Self::parse_enable_state(&kvs.lookup(ENABLE_KEY).unwrap_or_default(), false, false);
let scopes_str = kvs.get(OIDC_SCOPES);
let scopes = if scopes_str.is_empty() {
@@ -1053,12 +1067,8 @@ impl OidcSys {
.map(|s| s.to_string())
.collect();
let redirect_uri_dynamic = kvs
.lookup(OIDC_REDIRECT_URI_DYNAMIC)
.unwrap_or_else(|| EnableState::On.to_string())
.parse::<EnableState>()
.map(|s| s.is_enabled())
.unwrap_or(true);
let redirect_uri_dynamic =
Self::parse_enable_state(&kvs.lookup(OIDC_REDIRECT_URI_DYNAMIC).unwrap_or_default(), true, true);
let claim_name = kvs
.lookup(OIDC_CLAIM_NAME)
@@ -1078,6 +1088,7 @@ impl OidcSys {
let display_name = kvs.lookup(OIDC_DISPLAY_NAME).unwrap_or_else(|| id.to_string());
let redirect_uri = kvs.lookup(OIDC_REDIRECT_URI).filter(|v| !v.is_empty());
let client_secret = kvs.lookup(OIDC_CLIENT_SECRET).filter(|v| !v.is_empty());
let hide_from_ui = Self::parse_enable_state(&kvs.lookup(OIDC_HIDE_FROM_UI).unwrap_or_default(), false, false);
Some(OidcProviderConfig {
id: id.to_string(),
@@ -1097,6 +1108,7 @@ impl OidcSys {
roles_claim,
email_claim,
username_claim,
hide_from_ui,
})
}
@@ -1582,6 +1594,7 @@ mod tests {
roles_claim: String::new(),
email_claim: "email".to_string(),
username_claim: "username".to_string(),
hide_from_ui: false,
}
}
@@ -1961,9 +1974,198 @@ mod tests {
roles_claim: String::new(),
email_claim: "email".to_string(),
username_claim: "preferred_username".to_string(),
hide_from_ui: false,
}
}
#[test]
fn test_parse_enable_state_on() {
assert!(OidcSys::parse_enable_state("on", false, false));
}
#[test]
fn test_parse_enable_state_off() {
assert!(!OidcSys::parse_enable_state("off", true, true));
}
#[test]
fn test_parse_enable_state_empty_returns_default() {
assert!(OidcSys::parse_enable_state("", true, false));
assert!(!OidcSys::parse_enable_state("", false, true));
}
#[test]
fn test_parse_enable_state_invalid_returns_error_default() {
assert!(!OidcSys::parse_enable_state("garbage", true, false));
assert!(OidcSys::parse_enable_state("garbage", false, true));
}
#[test]
fn test_list_visible_providers_hides_hidden_provider() {
let visible = test_config("dex");
let mut hidden = test_config("kubernetes");
hidden.hide_from_ui = true;
let sys = make_test_sys(vec![visible, hidden]);
let listed = sys.list_visible_providers();
assert_eq!(listed.len(), 1);
assert!(listed.iter().any(|p| p.provider_id == "dex"));
assert!(!listed.iter().any(|p| p.provider_id == "kubernetes"));
}
#[test]
fn test_hidden_provider_still_resolvable_for_sts() {
let visible = test_config("dex");
let mut hidden = test_config("kubernetes");
hidden.hide_from_ui = true;
let sys = make_test_sys(vec![visible, hidden]);
assert!(sys.get_provider_config("kubernetes").is_some());
assert!(sys.get_provider_config("dex").is_some());
}
#[test]
fn test_list_providers_includes_hidden_for_replication() {
let visible = test_config("dex");
let mut hidden = test_config("kubernetes");
hidden.hide_from_ui = true;
let sys = make_test_sys(vec![visible, hidden]);
// Unfiltered list returns all (used by site-replication)
assert_eq!(sys.list_providers().len(), 2);
// UI-filtered list hides the hidden one
assert_eq!(sys.list_visible_providers().len(), 1);
}
#[test]
fn test_list_providers_all_visible_by_default() {
let a = test_config("okta");
let b = test_config("dex");
let sys = make_test_sys(vec![a, b]);
let listed = sys.list_visible_providers();
assert_eq!(listed.len(), 2);
}
#[test]
fn test_list_visible_providers_all_hidden() {
let mut a = test_config("k8s-a");
a.hide_from_ui = true;
let mut b = test_config("k8s-b");
b.hide_from_ui = true;
let sys = make_test_sys(vec![a, b]);
let listed = sys.list_visible_providers();
assert!(listed.is_empty());
assert!(sys.has_providers());
}
#[test]
fn test_hide_from_ui_default_is_false() {
let config = test_config("default");
assert!(!config.hide_from_ui);
}
#[test]
fn test_parse_persisted_hide_from_ui_off_is_false() {
let mut cfg = ServerConfig::new();
let mut kvs = KVS(vec![
rustfs_ecstore::config::KV {
key: ENABLE_KEY.to_string(),
value: EnableState::On.to_string(),
hidden_if_empty: false,
},
rustfs_ecstore::config::KV {
key: OIDC_CONFIG_URL.to_string(),
value: "https://example.com/.well-known/openid-configuration".to_string(),
hidden_if_empty: false,
},
rustfs_ecstore::config::KV {
key: OIDC_CLIENT_ID.to_string(),
value: "console".to_string(),
hidden_if_empty: false,
},
]);
kvs.insert(OIDC_HIDE_FROM_UI.to_string(), EnableState::Off.to_string());
cfg.0
.entry(IDENTITY_OPENID_SUB_SYS.to_string())
.or_default()
.insert(DEFAULT_DELIMITER.to_string(), kvs);
let parsed = OidcSys::parse_persisted_configs(&cfg);
assert_eq!(parsed.len(), 1);
assert!(!parsed[0].hide_from_ui);
}
#[test]
fn test_parse_persisted_hide_from_ui_missing_defaults_false() {
let mut cfg = ServerConfig::new();
let kvs = KVS(vec![
rustfs_ecstore::config::KV {
key: ENABLE_KEY.to_string(),
value: EnableState::On.to_string(),
hidden_if_empty: false,
},
rustfs_ecstore::config::KV {
key: OIDC_CONFIG_URL.to_string(),
value: "https://example.com/.well-known/openid-configuration".to_string(),
hidden_if_empty: false,
},
rustfs_ecstore::config::KV {
key: OIDC_CLIENT_ID.to_string(),
value: "console".to_string(),
hidden_if_empty: false,
},
]);
cfg.0
.entry(IDENTITY_OPENID_SUB_SYS.to_string())
.or_default()
.insert(DEFAULT_DELIMITER.to_string(), kvs);
let parsed = OidcSys::parse_persisted_configs(&cfg);
assert_eq!(parsed.len(), 1);
assert!(!parsed[0].hide_from_ui);
}
#[test]
fn test_parse_persisted_hide_from_ui() {
let mut cfg = ServerConfig::new();
let mut kvs = KVS(vec![
rustfs_ecstore::config::KV {
key: ENABLE_KEY.to_string(),
value: EnableState::On.to_string(),
hidden_if_empty: false,
},
rustfs_ecstore::config::KV {
key: OIDC_CONFIG_URL.to_string(),
value: "https://example.com/.well-known/openid-configuration".to_string(),
hidden_if_empty: false,
},
rustfs_ecstore::config::KV {
key: OIDC_CLIENT_ID.to_string(),
value: "console".to_string(),
hidden_if_empty: false,
},
]);
kvs.insert(OIDC_HIDE_FROM_UI.to_string(), EnableState::On.to_string());
cfg.0
.entry(IDENTITY_OPENID_SUB_SYS.to_string())
.or_default()
.insert(DEFAULT_DELIMITER.to_string(), kvs);
let parsed = OidcSys::parse_persisted_configs(&cfg);
assert_eq!(parsed.len(), 1);
assert!(parsed[0].hide_from_ui);
}
#[test]
fn test_map_claims_to_policies_with_provider() {
let mut config = test_config("okta");
@@ -2056,6 +2258,7 @@ mod tests {
roles_claim: String::new(),
email_claim: "email".to_string(),
username_claim: "preferred_username".to_string(),
hide_from_ui: false,
};
assert_eq!(config.id, "test");
+1 -1
View File
@@ -129,7 +129,7 @@ impl Config {
// Collect OIDC provider info if available
let oidc = rustfs_iam::get_oidc()
.map(|sys| {
sys.list_providers()
sys.list_visible_providers()
.into_iter()
.map(|p| OidcProviderInfo {
provider_id: p.provider_id,
+119 -80
View File
@@ -23,8 +23,8 @@ use matchit::Params;
use rustfs_config::oidc::{
IDENTITY_OPENID_SUB_SYS, OIDC_CLAIM_NAME, OIDC_CLAIM_PREFIX, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CONFIG_URL,
OIDC_DEFAULT_CLAIM_NAME, OIDC_DEFAULT_EMAIL_CLAIM, OIDC_DEFAULT_GROUPS_CLAIM, OIDC_DEFAULT_ROLES_CLAIM, OIDC_DEFAULT_SCOPES,
OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_OTHER_AUDIENCES, OIDC_REDIRECT_URI,
OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES, OIDC_USERNAME_CLAIM,
OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_HIDE_FROM_UI, OIDC_OTHER_AUDIENCES,
OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES, OIDC_USERNAME_CLAIM,
};
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
use rustfs_ecstore::config::com::{read_config_without_migrate, save_server_config};
@@ -143,6 +143,7 @@ struct OidcConfigView {
roles_claim: String,
email_claim: String,
username_claim: String,
hide_from_ui: bool,
}
#[derive(Debug, Serialize)]
@@ -180,6 +181,7 @@ struct OidcConfigUpsertRequest {
roles_claim: String,
email_claim: String,
username_claim: String,
hide_from_ui: bool,
}
impl Default for OidcConfigUpsertRequest {
@@ -201,6 +203,7 @@ impl Default for OidcConfigUpsertRequest {
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,
}
}
}
@@ -225,6 +228,7 @@ struct OidcConfigValidateRequest {
roles_claim: String,
email_claim: String,
username_claim: String,
hide_from_ui: bool,
}
impl Default for OidcConfigValidateRequest {
@@ -247,6 +251,7 @@ impl Default for OidcConfigValidateRequest {
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,
}
}
}
@@ -260,7 +265,7 @@ impl Operation for ListOidcProvidersHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let oidc_sys = rustfs_iam::get_oidc().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
let providers = oidc_sys.list_providers();
let providers = oidc_sys.list_visible_providers();
let json_body = serde_json::to_vec(&providers)
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("serialize error: {e}")))?;
@@ -302,6 +307,7 @@ impl Operation for GetOidcConfigHandler {
roles_claim: provider.config.roles_claim.clone(),
email_claim: provider.config.email_claim.clone(),
username_claim: provider.config.username_claim,
hide_from_ui: provider.config.hide_from_ui,
})
.collect();
@@ -852,60 +858,61 @@ fn validate_provider_config_fields(config: &rustfs_iam::oidc::OidcProviderConfig
Ok(())
}
fn or_default(value: &str, default: &str) -> String {
if value.trim().is_empty() {
default.to_string()
} else {
value.trim().to_string()
}
}
/// Normalize an `OidcProviderConfig` by trimming strings and applying defaults.
fn normalize_provider_config(mut config: rustfs_iam::oidc::OidcProviderConfig) -> rustfs_iam::oidc::OidcProviderConfig {
config.config_url = config.config_url.trim().to_string();
config.client_id = config.client_id.trim().to_string();
config.scopes = normalize_scopes(&config.scopes);
config.redirect_uri = normalize_optional(config.redirect_uri);
config.claim_name = or_default(&config.claim_name, OIDC_DEFAULT_CLAIM_NAME);
config.claim_prefix = config.claim_prefix.trim().to_string();
config.role_policy = config.role_policy.trim().to_string();
config.display_name = or_default(&config.display_name, &config.id);
config.groups_claim = or_default(&config.groups_claim, OIDC_DEFAULT_GROUPS_CLAIM);
config.roles_claim = or_default(&config.roles_claim, OIDC_DEFAULT_ROLES_CLAIM);
config.email_claim = or_default(&config.email_claim, OIDC_DEFAULT_EMAIL_CLAIM);
config.username_claim = or_default(&config.username_claim, OIDC_DEFAULT_USERNAME_CLAIM);
config
}
fn build_provider_config_from_upsert(
provider_id: &str,
request: OidcConfigUpsertRequest,
existing_secret: Option<String>,
) -> S3Result<rustfs_iam::oidc::OidcProviderConfig> {
let scopes = normalize_scopes(&request.scopes);
let client_secret = match request.client_secret {
Some(value) if !value.trim().is_empty() => Some(value),
_ => existing_secret.filter(|value| !value.trim().is_empty()),
};
let config = rustfs_iam::oidc::OidcProviderConfig {
let config = normalize_provider_config(rustfs_iam::oidc::OidcProviderConfig {
id: provider_id.to_string(),
enabled: request.enabled,
config_url: request.config_url.trim().to_string(),
client_id: request.client_id.trim().to_string(),
config_url: request.config_url,
client_id: request.client_id,
client_secret,
scopes,
scopes: request.scopes,
other_audiences: request.other_audiences,
redirect_uri: normalize_optional(request.redirect_uri),
redirect_uri: request.redirect_uri,
redirect_uri_dynamic: request.redirect_uri_dynamic,
claim_name: if request.claim_name.trim().is_empty() {
OIDC_DEFAULT_CLAIM_NAME.to_string()
} else {
request.claim_name.trim().to_string()
},
claim_prefix: request.claim_prefix.trim().to_string(),
role_policy: request.role_policy.trim().to_string(),
display_name: if request.display_name.trim().is_empty() {
provider_id.to_string()
} else {
request.display_name.trim().to_string()
},
groups_claim: if request.groups_claim.trim().is_empty() {
OIDC_DEFAULT_GROUPS_CLAIM.to_string()
} else {
request.groups_claim.trim().to_string()
},
roles_claim: if request.roles_claim.trim().is_empty() {
OIDC_DEFAULT_ROLES_CLAIM.to_string()
} else {
request.roles_claim.trim().to_string()
},
email_claim: if request.email_claim.trim().is_empty() {
OIDC_DEFAULT_EMAIL_CLAIM.to_string()
} else {
request.email_claim.trim().to_string()
},
username_claim: if request.username_claim.trim().is_empty() {
OIDC_DEFAULT_USERNAME_CLAIM.to_string()
} else {
request.username_claim.trim().to_string()
},
};
claim_name: request.claim_name,
claim_prefix: request.claim_prefix,
role_policy: request.role_policy,
display_name: request.display_name,
groups_claim: request.groups_claim,
roles_claim: request.roles_claim,
email_claim: request.email_claim,
username_claim: request.username_claim,
hide_from_ui: request.hide_from_ui,
});
validate_provider_config_fields(&config)?;
Ok(config)
@@ -915,49 +922,26 @@ fn build_provider_config_from_validate(
request: OidcConfigValidateRequest,
provider_id: &str,
) -> S3Result<rustfs_iam::oidc::OidcProviderConfig> {
let config = rustfs_iam::oidc::OidcProviderConfig {
let config = normalize_provider_config(rustfs_iam::oidc::OidcProviderConfig {
id: provider_id.to_string(),
enabled: request.enabled,
config_url: request.config_url.trim().to_string(),
client_id: request.client_id.trim().to_string(),
config_url: request.config_url,
client_id: request.client_id,
client_secret: request.client_secret.filter(|value| !value.trim().is_empty()),
scopes: normalize_scopes(&request.scopes),
scopes: request.scopes,
other_audiences: request.other_audiences,
redirect_uri: normalize_optional(request.redirect_uri),
redirect_uri: request.redirect_uri,
redirect_uri_dynamic: request.redirect_uri_dynamic,
claim_name: if request.claim_name.trim().is_empty() {
OIDC_DEFAULT_CLAIM_NAME.to_string()
} else {
request.claim_name.trim().to_string()
},
claim_prefix: request.claim_prefix.trim().to_string(),
role_policy: request.role_policy.trim().to_string(),
display_name: if request.display_name.trim().is_empty() {
provider_id.to_string()
} else {
request.display_name.trim().to_string()
},
groups_claim: if request.groups_claim.trim().is_empty() {
OIDC_DEFAULT_GROUPS_CLAIM.to_string()
} else {
request.groups_claim.trim().to_string()
},
roles_claim: if request.roles_claim.trim().is_empty() {
OIDC_DEFAULT_ROLES_CLAIM.to_string()
} else {
request.roles_claim.trim().to_string()
},
email_claim: if request.email_claim.trim().is_empty() {
OIDC_DEFAULT_EMAIL_CLAIM.to_string()
} else {
request.email_claim.trim().to_string()
},
username_claim: if request.username_claim.trim().is_empty() {
OIDC_DEFAULT_USERNAME_CLAIM.to_string()
} else {
request.username_claim.trim().to_string()
},
};
claim_name: request.claim_name,
claim_prefix: request.claim_prefix,
role_policy: request.role_policy,
display_name: request.display_name,
groups_claim: request.groups_claim,
roles_claim: request.roles_claim,
email_claim: request.email_claim,
username_claim: request.username_claim,
hide_from_ui: request.hide_from_ui,
});
validate_provider_config_fields(&config)?;
Ok(config)
@@ -1008,6 +992,15 @@ fn upsert_persisted_provider_config(config: &mut ServerConfig, provider_config:
set_kvs_value(&mut kvs, OIDC_ROLES_CLAIM, provider_config.roles_claim.clone());
set_kvs_value(&mut kvs, OIDC_EMAIL_CLAIM, provider_config.email_claim.clone());
set_kvs_value(&mut kvs, OIDC_USERNAME_CLAIM, provider_config.username_claim.clone());
set_kvs_value(
&mut kvs,
OIDC_HIDE_FROM_UI,
if provider_config.hide_from_ui {
EnableState::On.to_string()
} else {
EnableState::Off.to_string()
},
);
config
.0
@@ -1285,6 +1278,7 @@ mod tests {
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,
};
upsert_persisted_provider_config(&mut persisted_config, &provider_config);
@@ -1292,4 +1286,49 @@ mod tests {
assert!(oidc_restart_required_from_active_config(&persisted_config, Some(&active_config)));
assert!(!oidc_restart_required_from_active_config(&persisted_config, Some(&persisted_config)));
}
#[test]
fn test_upsert_persists_hide_from_ui_on() {
let mut config = ServerConfig::new();
let mut provider_config = rustfs_iam::oidc::OidcProviderConfig {
id: "kubernetes".to_string(),
enabled: true,
config_url: "https://example.com/.well-known/openid-configuration".to_string(),
client_id: "test".to_string(),
client_secret: None,
scopes: vec!["openid".to_string()],
other_audiences: vec![],
redirect_uri: None,
redirect_uri_dynamic: true,
claim_name: "sub".to_string(),
claim_prefix: String::new(),
role_policy: String::new(),
display_name: "Kubernetes".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: true,
};
upsert_persisted_provider_config(&mut config, &provider_config);
let kvs = config
.0
.get(IDENTITY_OPENID_SUB_SYS)
.and_then(|m| m.get("kubernetes"))
.expect("provider KVS should exist");
assert_eq!(kvs.get(OIDC_HIDE_FROM_UI), EnableState::On.to_string());
// Flip to false and verify
provider_config.hide_from_ui = false;
upsert_persisted_provider_config(&mut config, &provider_config);
let kvs = config
.0
.get(IDENTITY_OPENID_SUB_SYS)
.and_then(|m| m.get("kubernetes"))
.expect("provider KVS should exist");
assert_eq!(kvs.get(OIDC_HIDE_FROM_UI), EnableState::Off.to_string());
}
}