fix(oidc): support separate discovery issuer (#5149)

This commit is contained in:
Zhengchao An
2026-07-23 10:44:32 +08:00
committed by GitHub
parent 1c8088d0b2
commit d9e0a25174
6 changed files with 243 additions and 7 deletions
+9 -2
View File
@@ -55,10 +55,10 @@ use rustfs_config::notify::{
use rustfs_config::oidc::{
ENV_IDENTITY_OPENID_CLAIM_NAME, ENV_IDENTITY_OPENID_CLAIM_PREFIX, ENV_IDENTITY_OPENID_CLIENT_ID,
ENV_IDENTITY_OPENID_CLIENT_SECRET, ENV_IDENTITY_OPENID_CONFIG_URL, ENV_IDENTITY_OPENID_DISPLAY_NAME,
ENV_IDENTITY_OPENID_EMAIL_CLAIM, ENV_IDENTITY_OPENID_ENABLE, ENV_IDENTITY_OPENID_GROUPS_CLAIM,
ENV_IDENTITY_OPENID_EMAIL_CLAIM, ENV_IDENTITY_OPENID_ENABLE, ENV_IDENTITY_OPENID_GROUPS_CLAIM, ENV_IDENTITY_OPENID_ISSUER,
ENV_IDENTITY_OPENID_REDIRECT_URI, ENV_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC, ENV_IDENTITY_OPENID_ROLE_POLICY,
ENV_IDENTITY_OPENID_SCOPES, ENV_IDENTITY_OPENID_USERNAME_CLAIM, IDENTITY_OPENID_SUB_SYS, OIDC_CLAIM_NAME, OIDC_CLAIM_PREFIX,
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CONFIG_URL, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM,
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CONFIG_URL, OIDC_DISPLAY_NAME, OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_ISSUER,
OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_SCOPES, OIDC_USERNAME_CLAIM,
};
use rustfs_config::server_config::{Config as ServerConfig, DEFAULT_KVS, KV, KVS};
@@ -313,6 +313,12 @@ const OIDC_HELP_KEYS: &[HelpKeyMetadata] = &[
description: "openid discovery document URL e.g. \"https://accounts.google.com/.well-known/openid-configuration\"",
optional: false,
},
HelpKeyMetadata {
key: OIDC_ISSUER,
type_name: "url",
description: "expected OpenID issuer URL when it differs from config_url",
optional: true,
},
HelpKeyMetadata {
key: OIDC_CLIENT_ID,
type_name: "string",
@@ -1374,6 +1380,7 @@ fn env_help_key(sub_system: &str, key: &str) -> String {
(HEAL_SUB_SYS, HEAL_BITROT_CYCLE) => ENV_SCANNER_BITROT_CYCLE_SECS.to_string(),
(IDENTITY_OPENID_SUB_SYS, ENABLE_KEY) => ENV_IDENTITY_OPENID_ENABLE.to_string(),
(IDENTITY_OPENID_SUB_SYS, OIDC_CONFIG_URL) => ENV_IDENTITY_OPENID_CONFIG_URL.to_string(),
(IDENTITY_OPENID_SUB_SYS, OIDC_ISSUER) => ENV_IDENTITY_OPENID_ISSUER.to_string(),
(IDENTITY_OPENID_SUB_SYS, OIDC_CLIENT_ID) => ENV_IDENTITY_OPENID_CLIENT_ID.to_string(),
(IDENTITY_OPENID_SUB_SYS, OIDC_CLIENT_SECRET) => ENV_IDENTITY_OPENID_CLIENT_SECRET.to_string(),
(IDENTITY_OPENID_SUB_SYS, OIDC_SCOPES) => ENV_IDENTITY_OPENID_SCOPES.to_string(),
+64 -2
View File
@@ -31,8 +31,9 @@ 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_HIDE_FROM_UI, 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_ISSUER,
OIDC_OTHER_AUDIENCES, OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES,
OIDC_USERNAME_CLAIM,
};
use rustfs_config::server_config::Config as ServerConfig;
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_RUSTFS_BROWSER_REDIRECT_URL, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
@@ -164,6 +165,7 @@ struct OidcConfigView {
enabled: bool,
display_name: String,
config_url: String,
issuer: Option<String>,
client_id: String,
client_secret_configured: bool,
scopes: Vec<String>,
@@ -202,6 +204,7 @@ struct OidcConfigUpsertRequest {
enabled: bool,
display_name: String,
config_url: String,
issuer: Option<String>,
client_id: String,
client_secret: Option<String>,
scopes: Vec<String>,
@@ -224,6 +227,7 @@ impl Default for OidcConfigUpsertRequest {
enabled: true,
display_name: String::new(),
config_url: String::new(),
issuer: None,
client_id: String::new(),
client_secret: None,
scopes: OIDC_DEFAULT_SCOPES.split(',').map(ToString::to_string).collect(),
@@ -249,6 +253,7 @@ struct OidcConfigValidateRequest {
enabled: bool,
display_name: String,
config_url: String,
issuer: Option<String>,
client_id: String,
client_secret: Option<String>,
scopes: Vec<String>,
@@ -272,6 +277,7 @@ impl Default for OidcConfigValidateRequest {
enabled: true,
display_name: String::new(),
config_url: String::new(),
issuer: None,
client_id: String::new(),
client_secret: None,
scopes: OIDC_DEFAULT_SCOPES.split(',').map(ToString::to_string).collect(),
@@ -328,6 +334,7 @@ impl Operation for GetOidcConfigHandler {
enabled: provider.config.enabled,
display_name: provider.config.display_name.clone(),
config_url: provider.config.config_url.clone(),
issuer: provider.config.issuer.clone(),
client_id: provider.config.client_id.clone(),
client_secret_configured: provider.config.client_secret.is_some(),
scopes: provider.config.scopes.clone(),
@@ -990,6 +997,16 @@ fn validate_absolute_http_url(value: &str, field_name: &str) -> S3Result<()> {
Ok(())
}
fn validate_absolute_http_url_without_outbound_check(value: &str, field_name: &str) -> S3Result<()> {
let parsed = Url::parse(value).map_err(|_| s3_error!(InvalidRequest, "{} must be an absolute http/https URL", field_name))?;
if !is_valid_scheme(parsed.scheme()) || parsed.host_str().is_none() {
return Err(s3_error!(InvalidRequest, "{} must be an absolute http/https URL", field_name));
}
Ok(())
}
fn validate_provider_config_fields(config: &rustfs_iam::oidc::OidcProviderConfig) -> S3Result<()> {
if !is_valid_provider_id(&config.id) {
return Err(s3_error!(InvalidRequest, "invalid provider_id"));
@@ -998,6 +1015,9 @@ fn validate_provider_config_fields(config: &rustfs_iam::oidc::OidcProviderConfig
return Err(s3_error!(InvalidRequest, "config_url is required"));
}
validate_absolute_http_url(&config.config_url, "config_url")?;
if let Some(issuer) = config.issuer.as_deref() {
validate_absolute_http_url_without_outbound_check(issuer, "issuer")?;
}
if config.client_id.trim().is_empty() {
return Err(s3_error!(InvalidRequest, "client_id is required"));
@@ -1031,6 +1051,7 @@ fn or_default(value: &str, default: &str) -> 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.issuer = normalize_optional(config.issuer);
config.client_id = config.client_id.trim().to_string();
config.scopes = normalize_scopes(&config.scopes);
config.redirect_uri = normalize_optional(config.redirect_uri);
@@ -1059,6 +1080,7 @@ fn build_provider_config_from_upsert(
id: provider_id.to_string(),
enabled: request.enabled,
config_url: request.config_url,
issuer: request.issuer,
client_id: request.client_id,
client_secret,
scopes: request.scopes,
@@ -1088,6 +1110,7 @@ fn build_provider_config_from_validate(
id: provider_id.to_string(),
enabled: request.enabled,
config_url: request.config_url,
issuer: request.issuer,
client_id: request.client_id,
client_secret: request.client_secret.filter(|value| !value.trim().is_empty()),
scopes: request.scopes,
@@ -1132,6 +1155,7 @@ fn upsert_persisted_provider_config(config: &mut ServerConfig, provider_config:
},
);
set_kvs_value(&mut kvs, OIDC_CONFIG_URL, provider_config.config_url.clone());
set_kvs_value(&mut kvs, OIDC_ISSUER, provider_config.issuer.clone().unwrap_or_default());
set_kvs_value(&mut kvs, OIDC_CLIENT_ID, provider_config.client_id.clone());
set_kvs_value(&mut kvs, OIDC_CLIENT_SECRET, provider_config.client_secret.clone().unwrap_or_default());
set_kvs_value(&mut kvs, OIDC_SCOPES, provider_config.scopes.join(","));
@@ -1321,6 +1345,7 @@ mod tests {
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()],
@@ -1718,6 +1743,7 @@ mod tests {
id: "default".to_string(),
enabled: true,
config_url: "https://example.com/.well-known/openid-configuration".to_string(),
issuer: None,
client_id: "console".to_string(),
client_secret: Some("secret".to_string()),
scopes: vec!["openid".to_string(), "profile".to_string()],
@@ -1748,6 +1774,7 @@ mod tests {
id: "kubernetes".to_string(),
enabled: true,
config_url: "https://example.com/.well-known/openid-configuration".to_string(),
issuer: None,
client_id: "test".to_string(),
client_secret: None,
scopes: vec!["openid".to_string()],
@@ -1785,4 +1812,39 @@ mod tests {
.expect("provider KVS should exist");
assert_eq!(kvs.get(OIDC_HIDE_FROM_UI), EnableState::Off.to_string());
}
#[test]
fn test_upsert_persists_issuer() {
let mut config = ServerConfig::new();
let provider_config = rustfs_iam::oidc::OidcProviderConfig {
id: "kubernetes".to_string(),
enabled: true,
config_url: "http://keycloak.ns.svc.cluster.local:8080/realms/app/.well-known/openid-configuration".to_string(),
issuer: Some("https://app.local/realms/app".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: 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_ISSUER), "https://app.local/realms/app");
}
}