mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-13 08:02:03 +00:00
refactor(iam): separate OIDC authentication mapping (#8416)
This commit is contained in:
@@ -0,0 +1,500 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{FederatedAuthorization, VerifiedFederatedIdentity};
|
||||
use crate::{EVENT_OIDC_DIAGNOSTICS, LOG_COMPONENT_IAM, LOG_SUBSYSTEM_OIDC};
|
||||
use rustfs_policy::policy::{ClaimLookup, get_claim_case_insensitive};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FederatedAuthorizationRule {
|
||||
provider_id: String,
|
||||
claim_name: String,
|
||||
claim_prefix: String,
|
||||
role_policy: String,
|
||||
groups_claim: String,
|
||||
roles_claim: String,
|
||||
}
|
||||
|
||||
impl FederatedAuthorizationRule {
|
||||
pub(crate) fn new(
|
||||
provider_id: String,
|
||||
claim_name: String,
|
||||
claim_prefix: String,
|
||||
role_policy: String,
|
||||
groups_claim: String,
|
||||
roles_claim: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider_id,
|
||||
claim_name,
|
||||
claim_prefix,
|
||||
role_policy,
|
||||
groups_claim,
|
||||
roles_claim,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_ref(&self) -> FederatedAuthorizationRuleRef<'_> {
|
||||
FederatedAuthorizationRuleRef::new(
|
||||
&self.claim_name,
|
||||
&self.claim_prefix,
|
||||
&self.role_policy,
|
||||
&self.groups_claim,
|
||||
&self.roles_claim,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct FederatedAuthorizationRuleRef<'a> {
|
||||
claim_name: &'a str,
|
||||
claim_prefix: &'a str,
|
||||
role_policy: &'a str,
|
||||
groups_claim: &'a str,
|
||||
roles_claim: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> FederatedAuthorizationRuleRef<'a> {
|
||||
pub(crate) fn new(
|
||||
claim_name: &'a str,
|
||||
claim_prefix: &'a str,
|
||||
role_policy: &'a str,
|
||||
groups_claim: &'a str,
|
||||
roles_claim: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
claim_name,
|
||||
claim_prefix,
|
||||
role_policy,
|
||||
groups_claim,
|
||||
roles_claim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable provider-specific inputs for core federation authorization mapping.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FederatedAuthorizationRules {
|
||||
providers: Arc<HashMap<String, FederatedAuthorizationRule>>,
|
||||
}
|
||||
|
||||
impl FederatedAuthorizationRules {
|
||||
pub(crate) fn new(rules: impl IntoIterator<Item = FederatedAuthorizationRule>) -> Self {
|
||||
Self {
|
||||
providers: Arc::new(rules.into_iter().map(|rule| (rule.provider_id.clone(), rule)).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a verified external identity into RustFS authorization data.
|
||||
#[derive(Clone)]
|
||||
pub struct CoreFederatedAuthorizationMapper {
|
||||
rules: FederatedAuthorizationRules,
|
||||
}
|
||||
|
||||
impl CoreFederatedAuthorizationMapper {
|
||||
pub fn new(rules: FederatedAuthorizationRules) -> Self {
|
||||
Self { rules }
|
||||
}
|
||||
|
||||
pub(crate) fn map(&self, identity: VerifiedFederatedIdentity) -> FederatedAuthorization {
|
||||
let (provider, claims) = identity.into_parts();
|
||||
let provider_id = provider.into_string();
|
||||
let Some(rule) = self.rules.providers.get(&provider_id).map(FederatedAuthorizationRule::as_ref) else {
|
||||
return FederatedAuthorization {
|
||||
provider_id,
|
||||
claims,
|
||||
policies: Vec::new(),
|
||||
group_claim_policies: Vec::new(),
|
||||
groups: Vec::new(),
|
||||
roles_claim_key: None,
|
||||
roles: Vec::new(),
|
||||
};
|
||||
};
|
||||
let (policies, groups) = Self::map_policies_and_groups(&provider_id, rule, &claims.groups, &claims.raw, true);
|
||||
let group_claim_policies = Self::group_claim_policy_names(rule, &claims.groups, &claims.raw);
|
||||
let roles_claim_key = (!rule.roles_claim.trim().is_empty()).then(|| rule.roles_claim.trim().to_string());
|
||||
let roles = roles_claim_key
|
||||
.as_deref()
|
||||
.map(|claim_name| role_values(&claims.raw, claim_name))
|
||||
.unwrap_or_default();
|
||||
|
||||
FederatedAuthorization {
|
||||
provider_id,
|
||||
claims,
|
||||
policies,
|
||||
group_claim_policies,
|
||||
groups,
|
||||
roles_claim_key,
|
||||
roles,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_policies_and_groups(
|
||||
provider_id: &str,
|
||||
rule: FederatedAuthorizationRuleRef<'_>,
|
||||
source_groups: &[String],
|
||||
raw_claims: &HashMap<String, serde_json::Value>,
|
||||
emit_diagnostics: bool,
|
||||
) -> (Vec<String>, Vec<String>) {
|
||||
let has_role_policy = !rule.role_policy.trim().is_empty();
|
||||
let mut policies: Vec<String> = if has_role_policy {
|
||||
rule.role_policy
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|policy| !policy.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
} else {
|
||||
source_groups
|
||||
.iter()
|
||||
.map(|group| policy_name(rule.claim_prefix, group))
|
||||
.collect()
|
||||
};
|
||||
|
||||
if !has_role_policy && rule.claim_name != rule.groups_claim {
|
||||
policies.extend(
|
||||
claim_values(raw_claims, rule.claim_name)
|
||||
.into_iter()
|
||||
.map(|value| policy_name(rule.claim_prefix, &value)),
|
||||
);
|
||||
}
|
||||
policies.sort();
|
||||
policies.dedup();
|
||||
|
||||
let mut groups = source_groups.to_vec();
|
||||
groups.sort();
|
||||
groups.dedup();
|
||||
|
||||
if emit_diagnostics {
|
||||
record_mapping_diagnostics(provider_id, rule, raw_claims, &policies, &groups);
|
||||
}
|
||||
|
||||
(policies, groups)
|
||||
}
|
||||
|
||||
pub(crate) fn group_claim_policy_names(
|
||||
rule: FederatedAuthorizationRuleRef<'_>,
|
||||
source_groups: &[String],
|
||||
raw_claims: &HashMap<String, serde_json::Value>,
|
||||
) -> Vec<String> {
|
||||
if !rule.role_policy.trim().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let explicit_policy_names: Vec<String> = if rule.claim_name != rule.groups_claim {
|
||||
claim_values(raw_claims, rule.claim_name)
|
||||
.iter()
|
||||
.map(|value| policy_name(rule.claim_prefix, value))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut group_policies: Vec<String> = source_groups
|
||||
.iter()
|
||||
.map(|group| policy_name(rule.claim_prefix, group))
|
||||
.filter(|policy| !explicit_policy_names.contains(policy))
|
||||
.collect();
|
||||
group_policies.sort();
|
||||
group_policies.dedup();
|
||||
group_policies
|
||||
}
|
||||
}
|
||||
|
||||
fn record_mapping_diagnostics(
|
||||
provider_id: &str,
|
||||
rule: FederatedAuthorizationRuleRef<'_>,
|
||||
raw_claims: &HashMap<String, serde_json::Value>,
|
||||
policies: &[String],
|
||||
groups: &[String],
|
||||
) {
|
||||
let (claim_name_lookup, claim_name_type) = claim_lookup_details(raw_claims, rule.claim_name);
|
||||
let (groups_claim_lookup, groups_claim_type) = claim_lookup_details(raw_claims, rule.groups_claim);
|
||||
let (roles_claim_lookup, roles_claim_type) = claim_lookup_details(raw_claims, rule.roles_claim);
|
||||
let claim_name_value_count = claim_values(raw_claims, rule.claim_name).len();
|
||||
let groups_claim_value_count = claim_values(raw_claims, rule.groups_claim).len();
|
||||
let roles_claim_value_count = claim_values(raw_claims, rule.roles_claim).len();
|
||||
|
||||
debug!(
|
||||
event = EVENT_OIDC_DIAGNOSTICS,
|
||||
component = LOG_COMPONENT_IAM,
|
||||
subsystem = LOG_SUBSYSTEM_OIDC,
|
||||
result = "claims_policy_mapped",
|
||||
provider_id,
|
||||
claim_name = %rule.claim_name,
|
||||
claim_prefix = %rule.claim_prefix,
|
||||
groups_claim = %rule.groups_claim,
|
||||
roles_claim = %rule.roles_claim,
|
||||
role_policy_configured = !rule.role_policy.trim().is_empty(),
|
||||
policy_count = policies.len(),
|
||||
group_count = groups.len(),
|
||||
raw_claim_key_count = raw_claims.len(),
|
||||
claim_name_lookup,
|
||||
claim_name_type,
|
||||
claim_name_value_count,
|
||||
groups_claim_lookup,
|
||||
groups_claim_type,
|
||||
groups_claim_value_count,
|
||||
roles_claim_lookup,
|
||||
roles_claim_type,
|
||||
roles_claim_value_count,
|
||||
"oidc claims mapped to policies"
|
||||
);
|
||||
}
|
||||
|
||||
fn policy_name(prefix: &str, value: &str) -> String {
|
||||
format!("{prefix}{value}")
|
||||
}
|
||||
|
||||
fn claim_values(claims: &HashMap<String, serde_json::Value>, claim_name: &str) -> Vec<String> {
|
||||
match get_claim_case_insensitive(claims, claim_name) {
|
||||
ClaimLookup::Found(serde_json::Value::Array(values)) => values
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.collect(),
|
||||
ClaimLookup::Found(serde_json::Value::String(value)) => value.split(',').map(str::trim).map(ToOwned::to_owned).collect(),
|
||||
ClaimLookup::Missing | ClaimLookup::Ambiguous | ClaimLookup::Found(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn role_values(claims: &HashMap<String, serde_json::Value>, claim_name: &str) -> Vec<String> {
|
||||
match get_claim_case_insensitive(claims, claim_name) {
|
||||
ClaimLookup::Found(serde_json::Value::Array(values)) => values
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.collect(),
|
||||
ClaimLookup::Found(serde_json::Value::String(value)) => value
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect(),
|
||||
ClaimLookup::Missing | ClaimLookup::Ambiguous | ClaimLookup::Found(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn claim_lookup_details(claims: &HashMap<String, serde_json::Value>, claim_name: &str) -> (&'static str, &'static str) {
|
||||
match get_claim_case_insensitive(claims, claim_name) {
|
||||
ClaimLookup::Found(value) => ("found", claim_value_type(value)),
|
||||
ClaimLookup::Missing => ("missing", "none"),
|
||||
ClaimLookup::Ambiguous => ("ambiguous", "none"),
|
||||
}
|
||||
}
|
||||
|
||||
fn claim_value_type(value: &serde_json::Value) -> &'static str {
|
||||
match value {
|
||||
serde_json::Value::Null => "null",
|
||||
serde_json::Value::Bool(_) => "bool",
|
||||
serde_json::Value::Number(_) => "number",
|
||||
serde_json::Value::String(_) => "string",
|
||||
serde_json::Value::Array(_) => "array",
|
||||
serde_json::Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::federation::{FederatedClaims, FederatedProviderRef, VerifiedFederatedIdentity};
|
||||
use serde_json::json;
|
||||
|
||||
fn mapper(rule: FederatedAuthorizationRule) -> CoreFederatedAuthorizationMapper {
|
||||
CoreFederatedAuthorizationMapper::new(FederatedAuthorizationRules::new([rule]))
|
||||
}
|
||||
|
||||
fn identity(provider_id: &str) -> VerifiedFederatedIdentity {
|
||||
VerifiedFederatedIdentity::from_claims(
|
||||
FederatedProviderRef::new(provider_id.to_string()),
|
||||
FederatedClaims {
|
||||
sub: "subject".to_string(),
|
||||
email: "user@example.test".to_string(),
|
||||
username: "user".to_string(),
|
||||
groups: vec![
|
||||
"source-ops".to_string(),
|
||||
"source-developers".to_string(),
|
||||
"source-ops".to_string(),
|
||||
],
|
||||
raw: HashMap::from([
|
||||
("Policy".to_string(), json!(["explicit", "source-ops"])),
|
||||
("Roles".to_string(), json!("reader, writer, ,auditor")),
|
||||
]),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_mapping_preserves_policy_group_role_and_case_behavior() {
|
||||
let mapper = mapper(FederatedAuthorizationRule::new(
|
||||
"corp".to_string(),
|
||||
"policy".to_string(),
|
||||
"mapped-".to_string(),
|
||||
String::new(),
|
||||
"groups".to_string(),
|
||||
"roles".to_string(),
|
||||
));
|
||||
|
||||
let authorization = mapper.map(identity("corp"));
|
||||
|
||||
assert_eq!(
|
||||
authorization.policies,
|
||||
["mapped-explicit", "mapped-source-developers", "mapped-source-ops"]
|
||||
);
|
||||
assert_eq!(authorization.group_claim_policies, ["mapped-source-developers"]);
|
||||
assert_eq!(authorization.groups, ["source-developers", "source-ops"]);
|
||||
assert_eq!(authorization.roles_claim_key.as_deref(), Some("roles"));
|
||||
assert_eq!(authorization.roles, ["reader", "writer", "auditor"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_policy_mode_keeps_groups_without_group_derived_policies() {
|
||||
let mapper = mapper(FederatedAuthorizationRule::new(
|
||||
"corp".to_string(),
|
||||
"policy".to_string(),
|
||||
"mapped-".to_string(),
|
||||
" readonly, readwrite, readonly ".to_string(),
|
||||
"groups".to_string(),
|
||||
String::new(),
|
||||
));
|
||||
|
||||
let authorization = mapper.map(identity("corp"));
|
||||
|
||||
assert_eq!(authorization.policies, ["readonly", "readwrite"]);
|
||||
assert!(authorization.group_claim_policies.is_empty());
|
||||
assert_eq!(authorization.groups, ["source-developers", "source-ops"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_provider_rules_preserve_identity_without_authorization_context() {
|
||||
let authorization =
|
||||
CoreFederatedAuthorizationMapper::new(FederatedAuthorizationRules::default()).map(identity("missing"));
|
||||
|
||||
assert_eq!(authorization.provider_id, "missing");
|
||||
assert_eq!(authorization.claims.username, "user");
|
||||
assert!(!authorization.has_authorization_context());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_claim_name_wins_over_case_insensitive_candidates() {
|
||||
let mapper = mapper(FederatedAuthorizationRule::new(
|
||||
"corp".to_string(),
|
||||
"policy".to_string(),
|
||||
"mapped-".to_string(),
|
||||
String::new(),
|
||||
"groups".to_string(),
|
||||
String::new(),
|
||||
));
|
||||
let identity = VerifiedFederatedIdentity::from_claims(
|
||||
FederatedProviderRef::new("corp".to_string()),
|
||||
FederatedClaims {
|
||||
sub: String::new(),
|
||||
email: String::new(),
|
||||
username: String::new(),
|
||||
groups: Vec::new(),
|
||||
raw: HashMap::from([
|
||||
("policy".to_string(), json!("exact")),
|
||||
("Policy".to_string(), json!("fallback")),
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
let authorization = mapper.map(identity);
|
||||
|
||||
assert_eq!(authorization.policies, ["mapped-exact"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambiguous_case_insensitive_claim_name_is_ignored() {
|
||||
let mapper = mapper(FederatedAuthorizationRule::new(
|
||||
"corp".to_string(),
|
||||
"pOlIcY".to_string(),
|
||||
"mapped-".to_string(),
|
||||
String::new(),
|
||||
"groups".to_string(),
|
||||
String::new(),
|
||||
));
|
||||
let identity = VerifiedFederatedIdentity::from_claims(
|
||||
FederatedProviderRef::new("corp".to_string()),
|
||||
FederatedClaims {
|
||||
sub: String::new(),
|
||||
email: String::new(),
|
||||
username: String::new(),
|
||||
groups: vec!["source-ops".to_string()],
|
||||
raw: HashMap::from([
|
||||
("Policy".to_string(), json!("first")),
|
||||
("POLICY".to_string(), json!("second")),
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
let authorization = mapper.map(identity);
|
||||
|
||||
assert_eq!(authorization.policies, ["mapped-source-ops"]);
|
||||
assert_eq!(claim_lookup_details(&authorization.claims.raw, "pOlIcY"), ("ambiguous", "none"));
|
||||
assert_eq!(claim_lookup_details(&authorization.claims.raw, "missing"), ("missing", "none"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comma_separated_claim_values_keep_existing_empty_value_behavior() {
|
||||
let mapper = mapper(FederatedAuthorizationRule::new(
|
||||
"corp".to_string(),
|
||||
"Policy".to_string(),
|
||||
"mapped-".to_string(),
|
||||
String::new(),
|
||||
"groups".to_string(),
|
||||
String::new(),
|
||||
));
|
||||
let identity = VerifiedFederatedIdentity::from_claims(
|
||||
FederatedProviderRef::new("corp".to_string()),
|
||||
FederatedClaims {
|
||||
sub: String::new(),
|
||||
email: String::new(),
|
||||
username: String::new(),
|
||||
groups: Vec::new(),
|
||||
raw: HashMap::from([("Policy".to_string(), json!(" explicit, ,audit "))]),
|
||||
},
|
||||
);
|
||||
|
||||
let authorization = mapper.map(identity);
|
||||
|
||||
assert_eq!(authorization.policies, ["mapped-", "mapped-audit", "mapped-explicit"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_array_values_keep_existing_empty_value_behavior() {
|
||||
let mapper = mapper(FederatedAuthorizationRule::new(
|
||||
"corp".to_string(),
|
||||
"policy".to_string(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
"groups".to_string(),
|
||||
"roles".to_string(),
|
||||
));
|
||||
let identity = VerifiedFederatedIdentity::from_claims(
|
||||
FederatedProviderRef::new("corp".to_string()),
|
||||
FederatedClaims {
|
||||
sub: String::new(),
|
||||
email: String::new(),
|
||||
username: String::new(),
|
||||
groups: Vec::new(),
|
||||
raw: HashMap::from([("roles".to_string(), json!(["reader", "", 7, "writer"]))]),
|
||||
},
|
||||
);
|
||||
|
||||
let authorization = mapper.map(identity);
|
||||
|
||||
assert_eq!(authorization.roles, ["reader", "", "writer"]);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
mod binding;
|
||||
mod error;
|
||||
mod mapper;
|
||||
mod model;
|
||||
pub mod oidc;
|
||||
mod provider;
|
||||
@@ -22,10 +23,14 @@ mod transaction;
|
||||
|
||||
pub use binding::FederatedSessionBinding;
|
||||
pub use error::{FederatedSessionBindingError, FederationError, Result};
|
||||
pub use mapper::{CoreFederatedAuthorizationMapper, FederatedAuthorizationRules};
|
||||
pub(crate) use mapper::{FederatedAuthorizationRule, FederatedAuthorizationRuleRef};
|
||||
pub(crate) use model::OpaqueLogoutContinuation;
|
||||
pub use model::{
|
||||
FederatedAuthorization, FederatedClaims, FederatedCodeExchange, FederatedLoginSession, FederatedProviderRef,
|
||||
FederatedSession, FederatedSessionTransaction, OIDC_VIRTUAL_PARENT_CLAIM, VerifiedFederatedIdentity,
|
||||
};
|
||||
pub use provider::{FederatedIdentityProvider, FederatedProviderView, FederatedRedirectPolicy};
|
||||
pub(crate) use provider::{StandardOidcAuthentication, VerifiedFederatedCodeExchange};
|
||||
pub use registry::FederatedIdentityRegistry;
|
||||
pub use transaction::FederatedIdentityService;
|
||||
|
||||
@@ -56,6 +56,10 @@ impl FederatedProviderRef {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.provider_id
|
||||
}
|
||||
|
||||
pub(crate) fn into_string(self) -> String {
|
||||
self.provider_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -80,6 +84,10 @@ impl fmt::Debug for VerifiedFederatedIdentity {
|
||||
}
|
||||
|
||||
impl VerifiedFederatedIdentity {
|
||||
pub(crate) fn from_claims(provider: FederatedProviderRef, claims: FederatedClaims) -> Self {
|
||||
Self { provider, claims }
|
||||
}
|
||||
|
||||
pub fn provider(&self) -> &FederatedProviderRef {
|
||||
&self.provider
|
||||
}
|
||||
@@ -111,6 +119,10 @@ impl VerifiedFederatedIdentity {
|
||||
pub fn session_identity(&self) -> String {
|
||||
self.claims.session_identity()
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (FederatedProviderRef, FederatedClaims) {
|
||||
(self.provider, self.claims)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -193,6 +205,10 @@ pub(crate) struct OpaqueLogoutContinuation {
|
||||
}
|
||||
|
||||
impl OpaqueLogoutContinuation {
|
||||
pub(crate) fn new(provider: FederatedProviderRef, id_token: String) -> Self {
|
||||
Self { provider, id_token }
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (FederatedProviderRef, String) {
|
||||
(self.provider, self.id_token)
|
||||
}
|
||||
|
||||
@@ -12,111 +12,39 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::config::roles_claim_key;
|
||||
use crate::{
|
||||
federation::{FederatedAuthorization, FederatedClaims},
|
||||
oidc::{OidcClaims, OidcSys},
|
||||
federation::{FederatedClaims, FederatedProviderRef, VerifiedFederatedIdentity},
|
||||
oidc::OidcClaims,
|
||||
};
|
||||
use rustfs_policy::policy::{ClaimLookup, get_claim_case_insensitive};
|
||||
|
||||
fn string_list_claim(claims: &OidcClaims, claim_name: &str) -> Vec<String> {
|
||||
match get_claim_case_insensitive(&claims.raw, claim_name) {
|
||||
ClaimLookup::Found(serde_json::Value::Array(values)) => values
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.collect(),
|
||||
ClaimLookup::Found(serde_json::Value::String(value)) => value
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect(),
|
||||
ClaimLookup::Missing | ClaimLookup::Ambiguous | ClaimLookup::Found(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn authorization(oidc: &OidcSys, provider_id: String, claims: OidcClaims) -> FederatedAuthorization {
|
||||
let (policies, groups) = oidc.map_claims_to_policies(&provider_id, &claims);
|
||||
let group_claim_policies = oidc.group_claim_policy_names(&provider_id, &claims);
|
||||
let roles_claim_key = roles_claim_key(oidc, &provider_id);
|
||||
let roles = roles_claim_key
|
||||
.as_deref()
|
||||
.map(|claim_name| string_list_claim(&claims, claim_name))
|
||||
.unwrap_or_default();
|
||||
|
||||
FederatedAuthorization {
|
||||
provider_id,
|
||||
claims: FederatedClaims {
|
||||
pub(super) fn verified_identity(provider: FederatedProviderRef, claims: OidcClaims) -> VerifiedFederatedIdentity {
|
||||
VerifiedFederatedIdentity::from_claims(
|
||||
provider,
|
||||
FederatedClaims {
|
||||
sub: claims.sub,
|
||||
email: claims.email,
|
||||
username: claims.username,
|
||||
groups: claims.groups,
|
||||
raw: claims.raw,
|
||||
},
|
||||
policies,
|
||||
group_claim_policies,
|
||||
groups,
|
||||
roles_claim_key,
|
||||
roles,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::oidc::{make_test_sys, test_config};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn string_list_claim_matches_existing_array_and_csv_behavior() {
|
||||
let claims = OidcClaims {
|
||||
raw: HashMap::from([
|
||||
("array_roles".to_string(), json!(["reader", 7, "writer"])),
|
||||
("csv_roles".to_string(), json!("reader, writer, ,auditor")),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(string_list_claim(&claims, "array_roles"), ["reader", "writer"]);
|
||||
assert_eq!(string_list_claim(&claims, "csv_roles"), ["reader", "writer", "auditor"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_list_claim_preserves_exact_and_ambiguous_match_behavior() {
|
||||
let exact = OidcClaims {
|
||||
raw: HashMap::from([
|
||||
("Roles".to_string(), json!(["mixed-case"])),
|
||||
("roles".to_string(), json!(["exact-match"])),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(string_list_claim(&exact, "roles"), ["exact-match"]);
|
||||
|
||||
let ambiguous = OidcClaims {
|
||||
raw: HashMap::from([
|
||||
("Roles".to_string(), json!(["mixed-case"])),
|
||||
("ROLES".to_string(), json!(["upper-case"])),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(string_list_claim(&ambiguous, "roles").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authorization_preserves_verified_claims_and_keeps_source_groups_distinct() {
|
||||
let mut config = test_config("corp");
|
||||
config.claim_prefix = "mapped-".to_string();
|
||||
config.roles_claim = "roles".to_string();
|
||||
let oidc = make_test_sys(vec![config]);
|
||||
fn verified_identity_preserves_provider_and_claim_values() {
|
||||
let raw = HashMap::from([
|
||||
("iss".to_string(), json!("https://corp.example.test")),
|
||||
("department".to_string(), json!("engineering")),
|
||||
("roles".to_string(), json!(["reader", "admin"])),
|
||||
]);
|
||||
let authorization = authorization(
|
||||
&oidc,
|
||||
"corp".to_string(),
|
||||
let identity = verified_identity(
|
||||
FederatedProviderRef::new("corp".to_string()),
|
||||
OidcClaims {
|
||||
sub: " subject-123 ".to_string(),
|
||||
email: " user@example.test ".to_string(),
|
||||
@@ -130,16 +58,11 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(authorization.provider_id, "corp");
|
||||
assert_eq!(authorization.claims.sub, " subject-123 ");
|
||||
assert_eq!(authorization.claims.email, " user@example.test ");
|
||||
assert_eq!(authorization.claims.username, " user ");
|
||||
assert_eq!(authorization.claims.groups, ["source-ops", "source-developers", "source-ops"]);
|
||||
assert_eq!(authorization.claims.raw, raw);
|
||||
assert_eq!(authorization.policies, ["mapped-source-developers", "mapped-source-ops"]);
|
||||
assert_eq!(authorization.group_claim_policies, ["mapped-source-developers", "mapped-source-ops"]);
|
||||
assert_eq!(authorization.groups, ["source-developers", "source-ops"]);
|
||||
assert_eq!(authorization.roles_claim_key.as_deref(), Some("roles"));
|
||||
assert_eq!(authorization.roles, ["reader", "admin"]);
|
||||
assert_eq!(identity.provider().as_str(), "corp");
|
||||
assert_eq!(identity.subject(), " subject-123 ");
|
||||
assert_eq!(identity.email(), " user@example.test ");
|
||||
assert_eq!(identity.username(), " user ");
|
||||
assert_eq!(identity.source_groups(), ["source-ops", "source-developers", "source-ops"]);
|
||||
assert_eq!(identity.attributes(), &raw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,23 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{
|
||||
federation::FederatedRedirectPolicy,
|
||||
federation::{FederatedAuthorizationRule, FederatedAuthorizationRules, FederatedRedirectPolicy},
|
||||
oidc::{OidcConfigSnapshot, OidcSys},
|
||||
};
|
||||
|
||||
pub(crate) fn authorization_rules(oidc: &OidcSys) -> FederatedAuthorizationRules {
|
||||
FederatedAuthorizationRules::new(oidc.provider_configs().map(|config| {
|
||||
FederatedAuthorizationRule::new(
|
||||
config.id.clone(),
|
||||
config.claim_name.clone(),
|
||||
config.claim_prefix.clone(),
|
||||
config.role_policy.clone(),
|
||||
config.groups_claim.clone(),
|
||||
config.roles_claim.clone(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Read-only access to the active standard OIDC configuration.
|
||||
///
|
||||
/// This interface stays separate from authentication and generic provider
|
||||
@@ -32,22 +45,10 @@ pub(super) fn redirect_policy(oidc: &OidcSys, provider_id: &str) -> Option<Feder
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn roles_claim_key(oidc: &OidcSys, provider_id: &str) -> Option<String> {
|
||||
oidc.get_provider_config(provider_id)
|
||||
.map(|config| config.roles_claim.trim().to_string())
|
||||
.filter(|claim| !claim.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roles_claim_key_requires_explicit_provider_config() {
|
||||
let oidc = OidcSys::empty().expect("empty OIDC configuration should be valid");
|
||||
assert_eq!(roles_claim_key(&oidc, "default"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_policy_keeps_configured_uri_and_dynamic_setting() {
|
||||
let mut config = crate::oidc::test_config("default");
|
||||
|
||||
@@ -16,8 +16,10 @@ use super::config::OidcConfigQuery;
|
||||
use super::{claims, config, discovery, http};
|
||||
use crate::{
|
||||
federation::{
|
||||
FederatedAuthorization, FederatedCodeExchange, FederatedIdentityProvider, FederatedProviderView, FederatedRedirectPolicy,
|
||||
FederationError, Result,
|
||||
CoreFederatedAuthorizationMapper, FederatedAuthorization, FederatedAuthorizationRules, FederatedCodeExchange,
|
||||
FederatedIdentityProvider, FederatedIdentityService, FederatedProviderRef, FederatedProviderView,
|
||||
FederatedRedirectPolicy, FederationError, OpaqueLogoutContinuation, Result, StandardOidcAuthentication,
|
||||
VerifiedFederatedCodeExchange,
|
||||
},
|
||||
oidc::{OidcConfigSnapshot, OidcSys},
|
||||
};
|
||||
@@ -25,11 +27,26 @@ use std::sync::Arc;
|
||||
|
||||
pub struct StandardOidcAdapter {
|
||||
oidc: Arc<OidcSys>,
|
||||
authorization_rules: FederatedAuthorizationRules,
|
||||
}
|
||||
|
||||
impl StandardOidcAdapter {
|
||||
pub fn new(oidc: Arc<OidcSys>) -> Self {
|
||||
Self { oidc }
|
||||
let authorization_rules = config::authorization_rules(&oidc);
|
||||
Self {
|
||||
oidc,
|
||||
authorization_rules,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authorization_rules(&self) -> FederatedAuthorizationRules {
|
||||
self.authorization_rules.clone()
|
||||
}
|
||||
|
||||
pub fn into_service(self: Arc<Self>, mapper: CoreFederatedAuthorizationMapper) -> FederatedIdentityService {
|
||||
let authentication: Arc<dyn StandardOidcAuthentication> = self.clone();
|
||||
let provider: Arc<dyn FederatedIdentityProvider> = self;
|
||||
FederatedIdentityService::from_standard_oidc_parts(provider, authentication, mapper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +56,33 @@ impl OidcConfigQuery for StandardOidcAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StandardOidcAuthentication for StandardOidcAdapter {
|
||||
async fn exchange_identity(&self, state: &str, code: &str, redirect_uri: &str) -> Result<VerifiedFederatedCodeExchange> {
|
||||
let (oidc_claims, provider_id, session, id_token) = http::exchange_code(&self.oidc, state, code, redirect_uri).await?;
|
||||
let provider = FederatedProviderRef::new(provider_id);
|
||||
Ok(VerifiedFederatedCodeExchange::new(
|
||||
claims::verified_identity(provider.clone(), oidc_claims),
|
||||
session.redirect_after,
|
||||
OpaqueLogoutContinuation::new(provider, id_token),
|
||||
))
|
||||
}
|
||||
|
||||
async fn verify_identity(&self, jwt: &str) -> Result<crate::federation::VerifiedFederatedIdentity> {
|
||||
let (oidc_claims, provider_id) = self
|
||||
.oidc
|
||||
.verify_web_identity_token(jwt)
|
||||
.await
|
||||
.map_err(FederationError::TokenVerification)?;
|
||||
Ok(claims::verified_identity(FederatedProviderRef::new(provider_id), oidc_claims))
|
||||
}
|
||||
|
||||
async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result<String> {
|
||||
let (provider, id_token) = continuation.into_parts();
|
||||
http::create_logout_token(&self.oidc, provider.as_str(), &id_token).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FederatedIdentityProvider for StandardOidcAdapter {
|
||||
fn has_providers(&self) -> bool {
|
||||
@@ -66,21 +110,20 @@ impl FederatedIdentityProvider for StandardOidcAdapter {
|
||||
}
|
||||
|
||||
async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result<FederatedCodeExchange> {
|
||||
let (oidc_claims, provider_id, session, id_token) = http::exchange_code(&self.oidc, state, code, redirect_uri).await?;
|
||||
let exchange = StandardOidcAuthentication::exchange_identity(self, state, code, redirect_uri).await?;
|
||||
let (identity, redirect_after, continuation) = exchange.into_parts();
|
||||
let authorization = CoreFederatedAuthorizationMapper::new(self.authorization_rules()).map(identity);
|
||||
let (_, id_token) = continuation.into_parts();
|
||||
Ok(FederatedCodeExchange {
|
||||
authorization: claims::authorization(&self.oidc, provider_id, oidc_claims),
|
||||
redirect_after: session.redirect_after,
|
||||
authorization,
|
||||
redirect_after,
|
||||
id_token,
|
||||
})
|
||||
}
|
||||
|
||||
async fn verify_web_identity_token(&self, jwt: &str) -> Result<FederatedAuthorization> {
|
||||
let (oidc_claims, provider_id) = self
|
||||
.oidc
|
||||
.verify_web_identity_token(jwt)
|
||||
.await
|
||||
.map_err(FederationError::TokenVerification)?;
|
||||
Ok(claims::authorization(&self.oidc, provider_id, oidc_claims))
|
||||
let identity = StandardOidcAuthentication::verify_identity(self, jwt).await?;
|
||||
Ok(CoreFederatedAuthorizationMapper::new(self.authorization_rules()).map(identity))
|
||||
}
|
||||
|
||||
async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result<String> {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{FederatedAuthorization, FederatedCodeExchange, Result};
|
||||
use super::{FederatedAuthorization, FederatedCodeExchange, OpaqueLogoutContinuation, Result, VerifiedFederatedIdentity};
|
||||
use crate::oidc::OidcProviderConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -30,6 +30,37 @@ pub struct FederatedRedirectPolicy {
|
||||
pub allow_request_origin: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct VerifiedFederatedCodeExchange {
|
||||
identity: VerifiedFederatedIdentity,
|
||||
redirect_after: Option<String>,
|
||||
logout_continuation: OpaqueLogoutContinuation,
|
||||
}
|
||||
|
||||
impl VerifiedFederatedCodeExchange {
|
||||
pub(crate) fn new(
|
||||
identity: VerifiedFederatedIdentity,
|
||||
redirect_after: Option<String>,
|
||||
logout_continuation: OpaqueLogoutContinuation,
|
||||
) -> Self {
|
||||
Self {
|
||||
identity,
|
||||
redirect_after,
|
||||
logout_continuation,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (VerifiedFederatedIdentity, Option<String>, OpaqueLogoutContinuation) {
|
||||
(self.identity, self.redirect_after, self.logout_continuation)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub(crate) trait StandardOidcAuthentication: Send + Sync {
|
||||
async fn exchange_identity(&self, state: &str, code: &str, redirect_uri: &str) -> Result<VerifiedFederatedCodeExchange>;
|
||||
async fn verify_identity(&self, jwt: &str) -> Result<VerifiedFederatedIdentity>;
|
||||
async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result<String>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait FederatedIdentityProvider: Send + Sync {
|
||||
fn has_providers(&self) -> bool;
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
use super::FederatedIdentityProvider;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Immutable registry of built-in federation adapters.
|
||||
/// Compatibility facade for integrations built against the original provider API.
|
||||
/// Production standard OIDC startup composes the adapter, mapper, and service directly.
|
||||
pub struct FederatedIdentityRegistry {
|
||||
standard_oidc: Arc<dyn FederatedIdentityProvider>,
|
||||
}
|
||||
@@ -25,7 +26,7 @@ impl FederatedIdentityRegistry {
|
||||
Self { standard_oidc }
|
||||
}
|
||||
|
||||
pub(crate) fn standard_oidc(&self) -> &dyn FederatedIdentityProvider {
|
||||
self.standard_oidc.as_ref()
|
||||
pub(crate) fn standard_oidc_arc(&self) -> Arc<dyn FederatedIdentityProvider> {
|
||||
Arc::clone(&self.standard_oidc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::{
|
||||
FederatedIdentityRegistry, FederatedLoginSession, FederatedProviderView, FederatedRedirectPolicy, FederatedSession,
|
||||
FederatedSessionBinding, FederatedSessionTransaction, FederationError, Result,
|
||||
CoreFederatedAuthorizationMapper, FederatedAuthorization, FederatedIdentityProvider, FederatedIdentityRegistry,
|
||||
FederatedLoginSession, FederatedProviderView, FederatedRedirectPolicy, FederatedSession, FederatedSessionBinding,
|
||||
FederatedSessionTransaction, FederationError, OpaqueLogoutContinuation, Result, StandardOidcAuthentication,
|
||||
};
|
||||
use crate::oidc::OidcProviderConfig;
|
||||
use std::sync::Arc;
|
||||
|
||||
const DEFAULT_OIDC_PROVIDER_ID: &str = "default";
|
||||
|
||||
@@ -30,42 +32,108 @@ fn sorted_provider_views(mut providers: Vec<FederatedProviderView>) -> Vec<Feder
|
||||
}
|
||||
|
||||
pub struct FederatedIdentityService {
|
||||
registry: FederatedIdentityRegistry,
|
||||
provider: Arc<dyn FederatedIdentityProvider>,
|
||||
standard_oidc: Option<StandardOidcRuntime>,
|
||||
}
|
||||
|
||||
struct StandardOidcRuntime {
|
||||
authentication: Arc<dyn StandardOidcAuthentication>,
|
||||
mapper: CoreFederatedAuthorizationMapper,
|
||||
}
|
||||
|
||||
struct RuntimeCodeExchange {
|
||||
authorization: FederatedAuthorization,
|
||||
redirect_after: Option<String>,
|
||||
logout_continuation: OpaqueLogoutContinuation,
|
||||
}
|
||||
|
||||
impl FederatedIdentityService {
|
||||
async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result<RuntimeCodeExchange> {
|
||||
match &self.standard_oidc {
|
||||
Some(runtime) => {
|
||||
let exchange = runtime.authentication.exchange_identity(state, code, redirect_uri).await?;
|
||||
let (identity, redirect_after, logout_continuation) = exchange.into_parts();
|
||||
Ok(RuntimeCodeExchange {
|
||||
authorization: runtime.mapper.map(identity),
|
||||
redirect_after,
|
||||
logout_continuation,
|
||||
})
|
||||
}
|
||||
None => {
|
||||
let exchange = self.provider.exchange_code(state, code, redirect_uri).await?;
|
||||
let (authorization, redirect_after, logout_continuation) = exchange.into_parts();
|
||||
Ok(RuntimeCodeExchange {
|
||||
authorization,
|
||||
redirect_after,
|
||||
logout_continuation,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_identity(&self, jwt: &str) -> Result<FederatedAuthorization> {
|
||||
match &self.standard_oidc {
|
||||
Some(runtime) => Ok(runtime.mapper.map(runtime.authentication.verify_identity(jwt).await?)),
|
||||
None => self.provider.verify_web_identity_token(jwt).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result<String> {
|
||||
match &self.standard_oidc {
|
||||
Some(runtime) => runtime.authentication.create_logout_token(continuation).await,
|
||||
None => {
|
||||
let (provider, id_token) = continuation.into_parts();
|
||||
self.provider.create_logout_token(provider.as_str(), &id_token).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the compatibility service around the published provider interface.
|
||||
pub fn new(registry: FederatedIdentityRegistry) -> Self {
|
||||
Self { registry }
|
||||
Self {
|
||||
provider: registry.standard_oidc_arc(),
|
||||
standard_oidc: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_standard_oidc_parts(
|
||||
provider: Arc<dyn FederatedIdentityProvider>,
|
||||
authentication: Arc<dyn StandardOidcAuthentication>,
|
||||
mapper: CoreFederatedAuthorizationMapper,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
standard_oidc: Some(StandardOidcRuntime { authentication, mapper }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_providers(&self) -> bool {
|
||||
self.registry.standard_oidc().has_providers()
|
||||
self.provider.has_providers()
|
||||
}
|
||||
|
||||
pub fn list_providers(&self) -> Vec<FederatedProviderView> {
|
||||
sorted_provider_views(self.registry.standard_oidc().list_providers())
|
||||
let providers = self.provider.list_providers();
|
||||
sorted_provider_views(providers)
|
||||
}
|
||||
|
||||
pub fn list_visible_providers(&self) -> Vec<FederatedProviderView> {
|
||||
sorted_provider_views(self.registry.standard_oidc().list_visible_providers())
|
||||
let providers = self.provider.list_visible_providers();
|
||||
sorted_provider_views(providers)
|
||||
}
|
||||
|
||||
pub fn redirect_policy(&self, provider_id: &str) -> Option<FederatedRedirectPolicy> {
|
||||
self.registry.standard_oidc().redirect_policy(provider_id)
|
||||
self.provider.redirect_policy(provider_id)
|
||||
}
|
||||
|
||||
/// Compatibility accessor for existing federation consumers.
|
||||
/// Login redirects use [`Self::redirect_policy`], while OIDC-specific
|
||||
/// configuration consumers use the OIDC configuration query.
|
||||
pub fn get_provider_config(&self, provider_id: &str) -> Option<&OidcProviderConfig> {
|
||||
self.registry.standard_oidc().provider_config(provider_id)
|
||||
self.provider.provider_config(provider_id)
|
||||
}
|
||||
|
||||
pub async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option<String>) -> Result<String> {
|
||||
self.registry
|
||||
.standard_oidc()
|
||||
.authorize_url(provider_id, redirect_uri, redirect_after)
|
||||
.await
|
||||
self.provider.authorize_url(provider_id, redirect_uri, redirect_after).await
|
||||
}
|
||||
|
||||
pub async fn complete_authorization_code(
|
||||
@@ -76,27 +144,20 @@ impl FederatedIdentityService {
|
||||
duration_seconds: usize,
|
||||
binding: &dyn FederatedSessionBinding,
|
||||
) -> Result<FederatedLoginSession> {
|
||||
let exchange = self.registry.standard_oidc().exchange_code(state, code, redirect_uri).await?;
|
||||
let (authorization, redirect_after, logout_continuation) = exchange.into_parts();
|
||||
let exchange = self.exchange_code(state, code, redirect_uri).await?;
|
||||
let transaction = FederatedSessionTransaction {
|
||||
authorization,
|
||||
authorization: exchange.authorization,
|
||||
duration_seconds,
|
||||
session_policy: None,
|
||||
};
|
||||
let credentials = binding.bind(&transaction).await?;
|
||||
let (provider, id_token) = logout_continuation.into_parts();
|
||||
let logout_token = self
|
||||
.registry
|
||||
.standard_oidc()
|
||||
.create_logout_token(provider.as_str(), &id_token)
|
||||
.await?;
|
||||
|
||||
let logout_token = self.create_logout_token(exchange.logout_continuation).await?;
|
||||
Ok(FederatedLoginSession {
|
||||
session: FederatedSession {
|
||||
credentials,
|
||||
authorization: transaction.authorization,
|
||||
},
|
||||
redirect_after,
|
||||
redirect_after: exchange.redirect_after,
|
||||
logout_token,
|
||||
})
|
||||
}
|
||||
@@ -108,7 +169,7 @@ impl FederatedIdentityService {
|
||||
session_policy: Option<String>,
|
||||
binding: &dyn FederatedSessionBinding,
|
||||
) -> Result<FederatedSession> {
|
||||
let authorization = self.registry.standard_oidc().verify_web_identity_token(jwt).await?;
|
||||
let authorization = self.verify_identity(jwt).await?;
|
||||
if !authorization.has_authorization_context() {
|
||||
tracing::warn!(
|
||||
provider_id = %authorization.provider_id,
|
||||
@@ -144,10 +205,7 @@ impl FederatedIdentityService {
|
||||
}
|
||||
|
||||
pub async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result<Option<String>> {
|
||||
self.registry
|
||||
.standard_oidc()
|
||||
.build_logout_url(logout_token, post_logout_redirect_uri)
|
||||
.await
|
||||
self.provider.build_logout_url(logout_token, post_logout_redirect_uri).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +213,9 @@ impl FederatedIdentityService {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::federation::{
|
||||
FederatedAuthorization, FederatedClaims, FederatedCodeExchange, FederatedIdentityProvider, FederatedSessionBindingError,
|
||||
FederatedAuthorization, FederatedAuthorizationRule, FederatedAuthorizationRules, FederatedClaims, FederatedCodeExchange,
|
||||
FederatedIdentityProvider, FederatedProviderRef, FederatedSessionBindingError, VerifiedFederatedCodeExchange,
|
||||
VerifiedFederatedIdentity,
|
||||
};
|
||||
use rustfs_credentials::Credentials;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -224,6 +284,23 @@ mod tests {
|
||||
roles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn identity(&self, provider_id: &str) -> VerifiedFederatedIdentity {
|
||||
VerifiedFederatedIdentity::from_claims(
|
||||
FederatedProviderRef::new(provider_id.to_string()),
|
||||
FederatedClaims {
|
||||
sub: "subject".to_string(),
|
||||
email: String::new(),
|
||||
username: "user".to_string(),
|
||||
groups: if self.with_group {
|
||||
vec!["developers".to_string()]
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
raw: Default::default(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_views(provider_ids: &[&str]) -> Vec<FederatedProviderView> {
|
||||
@@ -300,10 +377,76 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StandardOidcAuthentication for TestProvider {
|
||||
async fn exchange_identity(
|
||||
&self,
|
||||
_state: &str,
|
||||
_code: &str,
|
||||
_redirect_uri: &str,
|
||||
) -> Result<VerifiedFederatedCodeExchange> {
|
||||
self.record("exchange");
|
||||
if self.failure == ProviderFailure::Exchange {
|
||||
return Err(FederationError::CodeExchange("exchange failed".to_string()));
|
||||
}
|
||||
let provider = FederatedProviderRef::new(self.browser_provider_id.to_string());
|
||||
Ok(VerifiedFederatedCodeExchange::new(
|
||||
self.identity(self.browser_provider_id),
|
||||
Some("/browser".to_string()),
|
||||
OpaqueLogoutContinuation::new(provider, "id-token".to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
async fn verify_identity(&self, _jwt: &str) -> Result<VerifiedFederatedIdentity> {
|
||||
self.record("verify");
|
||||
if self.failure == ProviderFailure::Verification {
|
||||
return Err(FederationError::TokenVerification("verification failed".to_string()));
|
||||
}
|
||||
Ok(self.identity(self.web_provider_id))
|
||||
}
|
||||
|
||||
async fn create_logout_token(&self, continuation: OpaqueLogoutContinuation) -> Result<String> {
|
||||
self.record("logout");
|
||||
let (provider, id_token) = continuation.into_parts();
|
||||
assert_eq!((provider.as_str(), id_token.as_str()), self.expected_logout);
|
||||
if self.failure == ProviderFailure::Logout {
|
||||
return Err(FederationError::Logout("logout failed".to_string()));
|
||||
}
|
||||
Ok("logout-token".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn standard_service(provider: Arc<TestProvider>) -> FederatedIdentityService {
|
||||
let mut provider_ids = vec![provider.browser_provider_id, provider.web_provider_id];
|
||||
provider_ids.sort_unstable();
|
||||
provider_ids.dedup();
|
||||
let role_policy = if provider.with_policy { "readwrite" } else { "" };
|
||||
let rules = FederatedAuthorizationRules::new(provider_ids.into_iter().map(|provider_id| {
|
||||
FederatedAuthorizationRule::new(
|
||||
provider_id.to_string(),
|
||||
"groups".to_string(),
|
||||
String::new(),
|
||||
role_policy.to_string(),
|
||||
"groups".to_string(),
|
||||
String::new(),
|
||||
)
|
||||
}));
|
||||
let authentication: Arc<dyn StandardOidcAuthentication> = provider.clone();
|
||||
let provider: Arc<dyn FederatedIdentityProvider> = provider;
|
||||
FederatedIdentityService {
|
||||
provider,
|
||||
standard_oidc: Some(StandardOidcRuntime {
|
||||
authentication,
|
||||
mapper: CoreFederatedAuthorizationMapper::new(rules),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
struct RecordingBinding {
|
||||
fail: bool,
|
||||
events: Arc<Mutex<Vec<&'static str>>>,
|
||||
transactions: Mutex<Vec<(String, usize, Option<String>)>>,
|
||||
mapped_policies: Mutex<Vec<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl RecordingBinding {
|
||||
@@ -312,6 +455,7 @@ mod tests {
|
||||
fail: false,
|
||||
events,
|
||||
transactions: Mutex::new(Vec::new()),
|
||||
mapped_policies: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +472,10 @@ mod tests {
|
||||
transaction.duration_seconds,
|
||||
transaction.session_policy.clone(),
|
||||
));
|
||||
self.mapped_policies
|
||||
.lock()
|
||||
.expect("mapped policies should not be poisoned")
|
||||
.push(transaction.authorization.policies.clone());
|
||||
if self.fail {
|
||||
return Err(FederatedSessionBindingError::Internal("binding failed".to_string()));
|
||||
}
|
||||
@@ -416,6 +564,107 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_runtime_maps_before_common_browser_and_web_identity_flows() {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut provider = TestProvider::new(events.clone());
|
||||
provider.browser_provider_id = "corp";
|
||||
provider.web_provider_id = "partner";
|
||||
provider.expected_logout = ("corp", "id-token");
|
||||
let binding = Arc::new(RecordingBinding::new(events.clone()));
|
||||
let service = standard_service(Arc::new(provider));
|
||||
|
||||
let login = service
|
||||
.complete_authorization_code("state", "code", "https://console.example/callback", 3600, binding.as_ref())
|
||||
.await
|
||||
.expect("standard callback flow should complete");
|
||||
assert_eq!(login.session.authorization.provider_id, "corp");
|
||||
assert_eq!(login.session.authorization.policies, ["readwrite"]);
|
||||
assert_eq!(
|
||||
events.lock().expect("event log should not be poisoned").as_slice(),
|
||||
["exchange", "bind", "logout"]
|
||||
);
|
||||
events.lock().expect("event log should not be poisoned").clear();
|
||||
|
||||
let web_identity = service
|
||||
.assume_role_with_web_identity("jwt", 7200, Some("session-policy".to_string()), binding.as_ref())
|
||||
.await
|
||||
.expect("standard web identity flow should complete");
|
||||
assert_eq!(web_identity.authorization.provider_id, "partner");
|
||||
assert_eq!(web_identity.authorization.policies, ["readwrite"]);
|
||||
assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), ["verify", "bind"]);
|
||||
assert_eq!(
|
||||
binding
|
||||
.mapped_policies
|
||||
.lock()
|
||||
.expect("mapped policies should not be poisoned")
|
||||
.as_slice(),
|
||||
[vec!["readwrite".to_string()], vec!["readwrite".to_string()]]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_runtime_checks_authorization_before_binding() {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut provider = TestProvider::new(events.clone());
|
||||
provider.with_policy = false;
|
||||
let binding = Arc::new(RecordingBinding::new(events.clone()));
|
||||
let service = standard_service(Arc::new(provider));
|
||||
|
||||
let error = service
|
||||
.assume_role_with_web_identity("jwt", 3600, None, binding.as_ref())
|
||||
.await
|
||||
.expect_err("standard web identity requires mapped authorization");
|
||||
|
||||
assert!(matches!(error, FederationError::NoAuthorizationContext));
|
||||
assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), ["verify"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_runtime_preserves_browser_failure_order() {
|
||||
for (provider_failure, binding_failure, expected_events) in [
|
||||
(ProviderFailure::Exchange, false, vec!["exchange"]),
|
||||
(ProviderFailure::None, true, vec!["exchange", "bind"]),
|
||||
(ProviderFailure::Logout, false, vec!["exchange", "bind", "logout"]),
|
||||
] {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut provider = TestProvider::new(events.clone());
|
||||
provider.failure = provider_failure;
|
||||
let mut binding = RecordingBinding::new(events.clone());
|
||||
binding.fail = binding_failure;
|
||||
let service = standard_service(Arc::new(provider));
|
||||
|
||||
service
|
||||
.complete_authorization_code("state", "code", "https://console.example/callback", 3600, &binding)
|
||||
.await
|
||||
.expect_err("the configured standard callback failure should be returned");
|
||||
|
||||
assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), expected_events);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_runtime_preserves_web_identity_failure_order() {
|
||||
for (provider_failure, binding_failure, expected_events) in [
|
||||
(ProviderFailure::Verification, false, vec!["verify"]),
|
||||
(ProviderFailure::None, true, vec!["verify", "bind"]),
|
||||
] {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut provider = TestProvider::new(events.clone());
|
||||
provider.failure = provider_failure;
|
||||
let mut binding = RecordingBinding::new(events.clone());
|
||||
binding.fail = binding_failure;
|
||||
let service = standard_service(Arc::new(provider));
|
||||
|
||||
service
|
||||
.assume_role_with_web_identity("jwt", 3600, None, &binding)
|
||||
.await
|
||||
.expect_err("the configured standard web identity failure should be returned");
|
||||
|
||||
assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), expected_events);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_identity_without_policy_or_group_is_not_bound() {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
@@ -20,10 +20,11 @@ use store::object::ObjectStore;
|
||||
use sys::IamSys;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
const LOG_COMPONENT_IAM: &str = "iam";
|
||||
pub(crate) const LOG_COMPONENT_IAM: &str = "iam";
|
||||
const LOG_SUBSYSTEM_RUNTIME: &str = "runtime";
|
||||
const LOG_SUBSYSTEM_OIDC: &str = "oidc";
|
||||
pub(crate) const LOG_SUBSYSTEM_OIDC: &str = "oidc";
|
||||
const EVENT_IAM_STATE: &str = "iam_state";
|
||||
pub(crate) const EVENT_OIDC_DIAGNOSTICS: &str = "oidc_diagnostics";
|
||||
const EVENT_OIDC_STATE: &str = "oidc_state";
|
||||
|
||||
pub mod cache;
|
||||
|
||||
+21
-133
@@ -18,7 +18,11 @@
|
||||
//! `openidconnect` crate for standards-compliant discovery, token exchange,
|
||||
//! and ID token verification.
|
||||
|
||||
use crate::oidc_state::{OidcAuthSession, OidcLogoutSession, OidcStateStore};
|
||||
use crate::{
|
||||
EVENT_OIDC_DIAGNOSTICS, LOG_COMPONENT_IAM, LOG_SUBSYSTEM_OIDC,
|
||||
federation::{CoreFederatedAuthorizationMapper, FederatedAuthorizationRuleRef},
|
||||
oidc_state::{OidcAuthSession, OidcLogoutSession, OidcStateStore},
|
||||
};
|
||||
use openidconnect::core::{
|
||||
CoreAuthenticationFlow, CoreClient, CoreIdToken, CoreIdTokenVerifier, CoreJsonWebKeySet, CoreJwsSigningAlgorithm,
|
||||
};
|
||||
@@ -47,9 +51,6 @@ use tokio::time::sleep;
|
||||
use tracing::{debug, error, warn};
|
||||
use url::Url;
|
||||
|
||||
const LOG_COMPONENT_IAM: &str = "iam";
|
||||
const LOG_SUBSYSTEM_OIDC: &str = "oidc";
|
||||
const EVENT_OIDC_DIAGNOSTICS: &str = "oidc_diagnostics";
|
||||
const EVENT_OIDC_HTTP: &str = "oidc_http";
|
||||
const OIDC_JWKS_REFRESH_INTERVAL: StdDuration = StdDuration::from_secs(24 * 60 * 60);
|
||||
const OIDC_DISCOVERY_TRANSPORT_RETRIES: usize = 3;
|
||||
@@ -1502,88 +1503,10 @@ impl OidcSys {
|
||||
|
||||
/// Map OIDC claims to rustfs policy names.
|
||||
pub fn map_claims_to_policies(&self, provider_id: &str, claims: &OidcClaims) -> (Vec<String>, Vec<String>) {
|
||||
let config = match self.get_provider_config(provider_id) {
|
||||
Some(c) => c,
|
||||
None => return (vec![], vec![]),
|
||||
let Some(rule) = self.authorization_rule(provider_id) else {
|
||||
return (Vec::new(), Vec::new());
|
||||
};
|
||||
|
||||
let mut policies = Vec::new();
|
||||
let mut groups = Vec::new();
|
||||
|
||||
// Role-policy and claim-based authorization are separate OIDC modes. When a
|
||||
// role policy is configured, group claims still provide group context but
|
||||
// must not also become policy names.
|
||||
let has_role_policy = !config.role_policy.trim().is_empty();
|
||||
if has_role_policy {
|
||||
for policy in config.role_policy.split(',') {
|
||||
let policy = policy.trim();
|
||||
if !policy.is_empty() {
|
||||
policies.push(policy.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for group in &claims.groups {
|
||||
groups.push(group.clone());
|
||||
if !has_role_policy {
|
||||
policies.push(claim_policy_name(&config.claim_prefix, group));
|
||||
}
|
||||
}
|
||||
|
||||
if !has_role_policy && config.claim_name != config.groups_claim {
|
||||
for val in extract_groups_claim(&claims.raw, &config.claim_name) {
|
||||
policies.push(claim_policy_name(&config.claim_prefix, &val));
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
policies.sort();
|
||||
policies.dedup();
|
||||
groups.sort();
|
||||
groups.dedup();
|
||||
|
||||
let mut raw_claim_keys: Vec<&str> = claims.raw.keys().map(String::as_str).collect();
|
||||
raw_claim_keys.sort_unstable();
|
||||
let (claim_name_lookup, claim_name_raw_value) = claim_lookup_for_log(&claims.raw, &config.claim_name);
|
||||
let (groups_claim_lookup, groups_claim_raw_value) = claim_lookup_for_log(&claims.raw, &config.groups_claim);
|
||||
let (roles_claim_lookup, roles_claim_raw_value) = claim_lookup_for_log(&claims.raw, &config.roles_claim);
|
||||
let claim_name_values = extract_groups_claim(&claims.raw, &config.claim_name);
|
||||
let groups_claim_values = extract_groups_claim(&claims.raw, &config.groups_claim);
|
||||
let roles_claim_values = extract_groups_claim(&claims.raw, &config.roles_claim);
|
||||
|
||||
debug!(
|
||||
event = EVENT_OIDC_DIAGNOSTICS,
|
||||
component = LOG_COMPONENT_IAM,
|
||||
subsystem = LOG_SUBSYSTEM_OIDC,
|
||||
result = "claims_policy_mapped",
|
||||
provider_id = %provider_id,
|
||||
claim_name = %config.claim_name,
|
||||
claim_prefix = %config.claim_prefix,
|
||||
groups_claim = %config.groups_claim,
|
||||
roles_claim = %config.roles_claim,
|
||||
role_policy = %config.role_policy,
|
||||
policy_count = policies.len(),
|
||||
group_count = groups.len(),
|
||||
policies = ?policies,
|
||||
groups = ?groups,
|
||||
raw_claim_keys = ?raw_claim_keys,
|
||||
raw_claims = ?claims.raw,
|
||||
claim_name_lookup = %claim_name_lookup,
|
||||
claim_name_type = claim_value_type_for_log(claim_name_raw_value),
|
||||
claim_name_value = ?claim_name_raw_value,
|
||||
claim_name_values = ?claim_name_values,
|
||||
groups_claim_lookup = %groups_claim_lookup,
|
||||
groups_claim_type = claim_value_type_for_log(groups_claim_raw_value),
|
||||
groups_claim_value = ?groups_claim_raw_value,
|
||||
groups_claim_values = ?groups_claim_values,
|
||||
roles_claim_lookup = %roles_claim_lookup,
|
||||
roles_claim_type = claim_value_type_for_log(roles_claim_raw_value),
|
||||
roles_claim_value = ?roles_claim_raw_value,
|
||||
roles_claim_values = ?roles_claim_values,
|
||||
"oidc claims mapped to policies"
|
||||
);
|
||||
|
||||
(policies, groups)
|
||||
CoreFederatedAuthorizationMapper::map_policies_and_groups(provider_id, rule, &claims.groups, &claims.raw, true)
|
||||
}
|
||||
|
||||
/// Policy names produced only by the canonical groups in claim-based mode: the groups claim
|
||||
@@ -1595,30 +1518,22 @@ impl OidcSys {
|
||||
/// dedicated policy claim are never included, so those configurations keep requiring every
|
||||
/// policy to resolve.
|
||||
pub fn group_claim_policy_names(&self, provider_id: &str, claims: &OidcClaims) -> Vec<String> {
|
||||
let Some(config) = self.get_provider_config(provider_id) else {
|
||||
let Some(rule) = self.authorization_rule(provider_id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !config.role_policy.trim().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
CoreFederatedAuthorizationMapper::group_claim_policy_names(rule, &claims.groups, &claims.raw)
|
||||
}
|
||||
|
||||
let explicit_policy_names: Vec<String> = if config.claim_name != config.groups_claim {
|
||||
extract_groups_claim(&claims.raw, &config.claim_name)
|
||||
.iter()
|
||||
.map(|value| claim_policy_name(&config.claim_prefix, value))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut policies: Vec<String> = claims
|
||||
.groups
|
||||
.iter()
|
||||
.map(|group| claim_policy_name(&config.claim_prefix, group))
|
||||
.filter(|policy| !explicit_policy_names.contains(policy))
|
||||
.collect();
|
||||
policies.sort();
|
||||
policies.dedup();
|
||||
policies
|
||||
fn authorization_rule(&self, provider_id: &str) -> Option<FederatedAuthorizationRuleRef<'_>> {
|
||||
self.get_provider_config(provider_id).map(|config| {
|
||||
FederatedAuthorizationRuleRef::new(
|
||||
&config.claim_name,
|
||||
&config.claim_prefix,
|
||||
&config.role_policy,
|
||||
&config.groups_claim,
|
||||
&config.roles_claim,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a raw JWT (id_token) for the AssumeRoleWithWebIdentity flow.
|
||||
@@ -2415,10 +2330,6 @@ fn extract_string_claim(claims: &HashMap<String, serde_json::Value>, key: &str)
|
||||
}
|
||||
}
|
||||
|
||||
fn claim_policy_name(claim_prefix: &str, value: &str) -> String {
|
||||
format!("{claim_prefix}{value}")
|
||||
}
|
||||
|
||||
/// Extract a groups/array claim from raw claims with case-insensitive fallback. Handles both string arrays and single strings.
|
||||
fn extract_groups_claim(claims: &HashMap<String, serde_json::Value>, key: &str) -> Vec<String> {
|
||||
match get_claim_case_insensitive(claims, key) {
|
||||
@@ -2443,29 +2354,6 @@ fn extract_canonical_group_values(
|
||||
groups
|
||||
}
|
||||
|
||||
fn claim_lookup_for_log<'a>(
|
||||
claims: &'a HashMap<String, serde_json::Value>,
|
||||
key: &str,
|
||||
) -> (&'static str, Option<&'a serde_json::Value>) {
|
||||
match get_claim_case_insensitive(claims, key) {
|
||||
ClaimLookup::Found(value) => ("found", Some(value)),
|
||||
ClaimLookup::Missing => ("missing", None),
|
||||
ClaimLookup::Ambiguous => ("ambiguous", None),
|
||||
}
|
||||
}
|
||||
|
||||
fn claim_value_type_for_log(value: Option<&serde_json::Value>) -> &'static str {
|
||||
match value {
|
||||
Some(serde_json::Value::Null) => "null",
|
||||
Some(serde_json::Value::Bool(_)) => "bool",
|
||||
Some(serde_json::Value::Number(_)) => "number",
|
||||
Some(serde_json::Value::String(_)) => "string",
|
||||
Some(serde_json::Value::Array(_)) => "array",
|
||||
Some(serde_json::Value::Object(_)) => "object",
|
||||
None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn make_test_sys(configs: Vec<OidcProviderConfig>) -> OidcSys {
|
||||
let configs = configs
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use rustfs_iam::{
|
||||
federation::{
|
||||
FederatedIdentityRegistry, FederatedIdentityService,
|
||||
CoreFederatedAuthorizationMapper, FederatedIdentityService,
|
||||
oidc::{OidcConfigQuery, StandardOidcAdapter},
|
||||
},
|
||||
get_oidc, init_oidc_sys_with_extra_root_ca_provider,
|
||||
@@ -80,8 +80,8 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
|
||||
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)), oidc_config_query)
|
||||
let mapper = CoreFederatedAuthorizationMapper::new(adapter.authorization_rules());
|
||||
(Arc::new(adapter.into_service(mapper)), oidc_config_query)
|
||||
}
|
||||
|
||||
pub(crate) fn oidc_extra_root_ca_provider() -> OidcExtraRootCaProvider {
|
||||
|
||||
Reference in New Issue
Block a user