mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
feat(oidc): add roles_claim and jwt:roles policy support (#2509)
Co-authored-by: GatewayJ <8352692332qq.com> Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
@@ -24,6 +24,7 @@ pub const OIDC_CLAIM_PREFIX: &str = "claim_prefix";
|
||||
pub const OIDC_ROLE_POLICY: &str = "role_policy";
|
||||
pub const OIDC_DISPLAY_NAME: &str = "display_name";
|
||||
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";
|
||||
|
||||
@@ -40,11 +41,12 @@ pub const ENV_IDENTITY_OPENID_CLAIM_PREFIX: &str = "RUSTFS_IDENTITY_OPENID_CLAIM
|
||||
pub const ENV_IDENTITY_OPENID_ROLE_POLICY: &str = "RUSTFS_IDENTITY_OPENID_ROLE_POLICY";
|
||||
pub const ENV_IDENTITY_OPENID_DISPLAY_NAME: &str = "RUSTFS_IDENTITY_OPENID_DISPLAY_NAME";
|
||||
pub const ENV_IDENTITY_OPENID_GROUPS_CLAIM: &str = "RUSTFS_IDENTITY_OPENID_GROUPS_CLAIM";
|
||||
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";
|
||||
|
||||
/// List of all environment variable keys for an OIDC provider.
|
||||
pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 14] = &[
|
||||
pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 15] = &[
|
||||
ENV_IDENTITY_OPENID_ENABLE,
|
||||
ENV_IDENTITY_OPENID_CONFIG_URL,
|
||||
ENV_IDENTITY_OPENID_CLIENT_ID,
|
||||
@@ -57,6 +59,7 @@ pub const ENV_IDENTITY_OPENID_KEYS: &[&str; 14] = &[
|
||||
ENV_IDENTITY_OPENID_ROLE_POLICY,
|
||||
ENV_IDENTITY_OPENID_DISPLAY_NAME,
|
||||
ENV_IDENTITY_OPENID_GROUPS_CLAIM,
|
||||
ENV_IDENTITY_OPENID_ROLES_CLAIM,
|
||||
ENV_IDENTITY_OPENID_EMAIL_CLAIM,
|
||||
ENV_IDENTITY_OPENID_USERNAME_CLAIM,
|
||||
];
|
||||
@@ -75,6 +78,7 @@ pub const IDENTITY_OPENID_KEYS: &[&str] = &[
|
||||
OIDC_ROLE_POLICY,
|
||||
OIDC_DISPLAY_NAME,
|
||||
OIDC_GROUPS_CLAIM,
|
||||
OIDC_ROLES_CLAIM,
|
||||
OIDC_EMAIL_CLAIM,
|
||||
OIDC_USERNAME_CLAIM,
|
||||
crate::COMMENT_KEY,
|
||||
@@ -84,6 +88,8 @@ pub const IDENTITY_OPENID_KEYS: &[&str] = &[
|
||||
pub const OIDC_DEFAULT_SCOPES: &str = "openid,profile,email";
|
||||
pub const OIDC_DEFAULT_CLAIM_NAME: &str = "groups";
|
||||
pub const OIDC_DEFAULT_GROUPS_CLAIM: &str = "groups";
|
||||
/// Empty means do not merge a secondary claim into groups (legacy behavior). Set to e.g. `roles` to opt in.
|
||||
pub const OIDC_DEFAULT_ROLES_CLAIM: &str = "";
|
||||
pub const OIDC_DEFAULT_EMAIL_CLAIM: &str = "email";
|
||||
pub const OIDC_DEFAULT_USERNAME_CLAIM: &str = "preferred_username";
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ use rustfs_config::{
|
||||
ENABLE_KEY, EnableState,
|
||||
oidc::{
|
||||
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_SCOPES, OIDC_DEFAULT_USERNAME_CLAIM, OIDC_DISPLAY_NAME,
|
||||
OIDC_EMAIL_CLAIM, OIDC_GROUPS_CLAIM, OIDC_REDIRECT_URI, OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_SCOPES,
|
||||
OIDC_USERNAME_CLAIM,
|
||||
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_REDIRECT_URI,
|
||||
OIDC_REDIRECT_URI_DYNAMIC, OIDC_ROLE_POLICY, OIDC_ROLES_CLAIM, OIDC_SCOPES, OIDC_USERNAME_CLAIM,
|
||||
},
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
@@ -87,6 +87,11 @@ pub static DEFAULT_IDENTITY_OPENID_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||
value: OIDC_DEFAULT_GROUPS_CLAIM.to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: OIDC_ROLES_CLAIM.to_owned(),
|
||||
value: OIDC_DEFAULT_ROLES_CLAIM.to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: OIDC_EMAIL_CLAIM.to_owned(),
|
||||
value: OIDC_DEFAULT_EMAIL_CLAIM.to_owned(),
|
||||
|
||||
+94
-2
@@ -119,6 +119,7 @@ pub struct OidcProviderConfig {
|
||||
pub role_policy: String,
|
||||
pub display_name: String,
|
||||
pub groups_claim: String,
|
||||
pub roles_claim: String,
|
||||
pub email_claim: String,
|
||||
pub username_claim: String,
|
||||
}
|
||||
@@ -385,7 +386,7 @@ impl OidcSys {
|
||||
sub: extract_string_claim(&raw, "sub"),
|
||||
email: extract_string_claim(&raw, &config.email_claim),
|
||||
username: extract_string_claim(&raw, &config.username_claim),
|
||||
groups: extract_groups_claim(&raw, &config.groups_claim),
|
||||
groups: extract_canonical_group_values(&raw, &config.groups_claim, &config.roles_claim),
|
||||
raw,
|
||||
};
|
||||
|
||||
@@ -509,7 +510,7 @@ impl OidcSys {
|
||||
sub: extract_string_claim(&raw_claims, "sub"),
|
||||
email: extract_string_claim(&raw_claims, &config.email_claim),
|
||||
username: extract_string_claim(&raw_claims, &config.username_claim),
|
||||
groups: extract_groups_claim(&raw_claims, &config.groups_claim),
|
||||
groups: extract_canonical_group_values(&raw_claims, &config.groups_claim, &config.roles_claim),
|
||||
raw: raw_claims,
|
||||
};
|
||||
|
||||
@@ -719,6 +720,7 @@ impl OidcSys {
|
||||
v
|
||||
}
|
||||
};
|
||||
let roles_claim = get_env(ENV_IDENTITY_OPENID_ROLES_CLAIM);
|
||||
let email_claim = {
|
||||
let v = get_env(ENV_IDENTITY_OPENID_EMAIL_CLAIM);
|
||||
if v.is_empty() {
|
||||
@@ -762,6 +764,7 @@ impl OidcSys {
|
||||
role_policy: get_env(ENV_IDENTITY_OPENID_ROLE_POLICY),
|
||||
display_name,
|
||||
groups_claim,
|
||||
roles_claim,
|
||||
email_claim,
|
||||
username_claim,
|
||||
})
|
||||
@@ -800,6 +803,9 @@ impl OidcSys {
|
||||
let groups_claim = kvs
|
||||
.lookup(OIDC_GROUPS_CLAIM)
|
||||
.unwrap_or_else(|| OIDC_DEFAULT_GROUPS_CLAIM.to_string());
|
||||
let roles_claim = kvs
|
||||
.lookup(OIDC_ROLES_CLAIM)
|
||||
.unwrap_or_else(|| OIDC_DEFAULT_ROLES_CLAIM.to_string());
|
||||
let email_claim = kvs
|
||||
.lookup(OIDC_EMAIL_CLAIM)
|
||||
.unwrap_or_else(|| OIDC_DEFAULT_EMAIL_CLAIM.to_string());
|
||||
@@ -824,6 +830,7 @@ impl OidcSys {
|
||||
role_policy: kvs.get(OIDC_ROLE_POLICY),
|
||||
display_name,
|
||||
groups_claim,
|
||||
roles_claim,
|
||||
email_claim,
|
||||
username_claim,
|
||||
})
|
||||
@@ -1025,6 +1032,21 @@ fn extract_groups_claim(claims: &HashMap<String, serde_json::Value>, key: &str)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_canonical_group_values(
|
||||
claims: &HashMap<String, serde_json::Value>,
|
||||
groups_claim: &str,
|
||||
roles_claim: &str,
|
||||
) -> Vec<String> {
|
||||
let mut groups = extract_groups_claim(claims, groups_claim);
|
||||
if !roles_claim.is_empty() && roles_claim != groups_claim {
|
||||
groups.extend(extract_groups_claim(claims, roles_claim));
|
||||
}
|
||||
groups.retain(|g| !g.is_empty());
|
||||
groups.sort();
|
||||
groups.dedup();
|
||||
groups
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1073,6 +1095,34 @@ mod tests {
|
||||
assert!(groups.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_canonical_group_values_merges_groups_and_roles() {
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert("groups".to_string(), serde_json::json!(["devs", "admins"]));
|
||||
claims.insert("roles".to_string(), serde_json::json!(["admins", "consoleAdmin"]));
|
||||
|
||||
let merged = extract_canonical_group_values(&claims, "groups", "roles");
|
||||
assert_eq!(merged, vec!["admins", "consoleAdmin", "devs"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_canonical_group_values_skips_duplicate_claim_name() {
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert("roles".to_string(), serde_json::json!(["consoleAdmin"]));
|
||||
|
||||
let merged = extract_canonical_group_values(&claims, "roles", "roles");
|
||||
assert_eq!(merged, vec!["consoleAdmin"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_canonical_group_values_roles_only() {
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert("roles".to_string(), serde_json::json!(["consoleAdmin", "bucket-reader"]));
|
||||
|
||||
let merged = extract_canonical_group_values(&claims, "groups", "roles");
|
||||
assert_eq!(merged, vec!["bucket-reader", "consoleAdmin"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_string_claim_case_insensitive() {
|
||||
let mut claims = HashMap::new();
|
||||
@@ -1246,6 +1296,7 @@ mod tests {
|
||||
role_policy: String::new(),
|
||||
display_name: "mock-oidc".to_string(),
|
||||
groups_claim: "groups".to_string(),
|
||||
roles_claim: String::new(),
|
||||
email_claim: "email".to_string(),
|
||||
username_claim: "username".to_string(),
|
||||
}
|
||||
@@ -1491,6 +1542,7 @@ mod tests {
|
||||
);
|
||||
kvs.insert(OIDC_CLIENT_ID.to_string(), "console".to_string());
|
||||
kvs.insert(ENABLE_KEY.to_string(), EnableState::On.to_string());
|
||||
kvs.insert(OIDC_ROLES_CLAIM.to_string(), "app_roles".to_string());
|
||||
|
||||
cfg.0
|
||||
.entry(IDENTITY_OPENID_SUB_SYS.to_string())
|
||||
@@ -1502,6 +1554,44 @@ mod tests {
|
||||
assert_eq!(parsed[0].id, "default");
|
||||
assert_eq!(parsed[0].client_id, "console");
|
||||
assert!(parsed[0].enabled);
|
||||
assert_eq!(parsed[0].roles_claim, "app_roles");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_persisted_provider_config_omitted_roles_claim_is_empty() {
|
||||
let mut cfg = ServerConfig::new();
|
||||
let mut kvs = KVS(vec![
|
||||
rustfs_ecstore::config::KV {
|
||||
key: ENABLE_KEY.to_string(),
|
||||
value: EnableState::Off.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
rustfs_ecstore::config::KV {
|
||||
key: OIDC_CONFIG_URL.to_string(),
|
||||
value: String::new(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
rustfs_ecstore::config::KV {
|
||||
key: OIDC_CLIENT_ID.to_string(),
|
||||
value: String::new(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
]);
|
||||
kvs.insert(
|
||||
OIDC_CONFIG_URL.to_string(),
|
||||
"https://example.com/.well-known/openid-configuration".to_string(),
|
||||
);
|
||||
kvs.insert(OIDC_CLIENT_ID.to_string(), "console".to_string());
|
||||
kvs.insert(ENABLE_KEY.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_eq!(parsed[0].roles_claim, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1554,6 +1644,7 @@ mod tests {
|
||||
role_policy: "".to_string(),
|
||||
display_name: id.to_string(),
|
||||
groups_claim: "groups".to_string(),
|
||||
roles_claim: String::new(),
|
||||
email_claim: "email".to_string(),
|
||||
username_claim: "preferred_username".to_string(),
|
||||
}
|
||||
@@ -1647,6 +1738,7 @@ mod tests {
|
||||
role_policy: "readwrite".to_string(),
|
||||
display_name: "Test Provider".to_string(),
|
||||
groups_claim: "groups".to_string(),
|
||||
roles_claim: String::new(),
|
||||
email_claim: "email".to_string(),
|
||||
username_claim: "preferred_username".to_string(),
|
||||
};
|
||||
|
||||
@@ -79,6 +79,7 @@ impl KeyName {
|
||||
KeyName::Jwt(JwtKeyName::JWTName),
|
||||
KeyName::Jwt(JwtKeyName::JWTUpn),
|
||||
KeyName::Jwt(JwtKeyName::JWTGroups),
|
||||
KeyName::Jwt(JwtKeyName::JWTRoles),
|
||||
KeyName::Jwt(JwtKeyName::JWTGivenName),
|
||||
KeyName::Jwt(JwtKeyName::JWTFamilyName),
|
||||
KeyName::Jwt(JwtKeyName::JWTMiddleName),
|
||||
@@ -231,6 +232,9 @@ pub enum JwtKeyName {
|
||||
#[strum(serialize = "jwt:groups")]
|
||||
JWTGroups,
|
||||
|
||||
#[strum(serialize = "jwt:roles")]
|
||||
JWTRoles,
|
||||
|
||||
#[strum(serialize = "jwt:given_name")]
|
||||
JWTGivenName,
|
||||
|
||||
@@ -403,4 +407,9 @@ mod tests {
|
||||
let data = serde_json::to_string(&TestCase { data: value }).expect("marshal failed");
|
||||
assert_eq!(data, except);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_name_from_str_supports_jwt_roles() {
|
||||
assert!(KeyName::try_from("jwt:roles").is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,31 @@ mod tests {
|
||||
pollster::block_on(result) ^ negate
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jwt_roles_condition_uses_roles_values() {
|
||||
assert!(test_eval(
|
||||
new_fkv("jwt:roles", vec!["RustFS.ConsoleAdmin"]),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
vec![("roles", vec!["RustFS.ConsoleAdmin"])]
|
||||
));
|
||||
assert!(!test_eval(
|
||||
new_fkv("jwt:roles", vec!["RustFS.ConsoleAdmin"]),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
vec![("roles", vec!["readonly"])]
|
||||
));
|
||||
assert!(!test_eval(
|
||||
new_fkv("jwt:roles", vec!["RustFS.ConsoleAdmin"]),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
vec![("groups", vec!["RustFS.ConsoleAdmin"])]
|
||||
));
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")]
|
||||
|
||||
Reference in New Issue
Block a user