mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
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:
committed by
GitHub
parent
be98c1f86a
commit
ce6fcf39b1
@@ -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
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user