refactor(iam): introduce federated identity boundary (#5018)

This commit is contained in:
GatewayJ
2026-07-19 14:28:08 +08:00
committed by GitHub
parent 7f5873dac8
commit f9e8440a04
29 changed files with 1707 additions and 554 deletions
+102
View File
@@ -0,0 +1,102 @@
// 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::config::roles_claim_key;
use crate::{
federation::{FederatedAuthorization, FederatedClaims},
oidc::{OidcClaims, OidcSys},
};
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 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 {
sub: claims.sub,
email: claims.email,
username: claims.username,
groups: claims.groups,
raw: claims.raw,
},
policies,
groups,
roles_claim_key,
roles,
}
}
#[cfg(test)]
mod tests {
use super::*;
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());
}
}
+36
View File
@@ -0,0 +1,36 @@
// 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 crate::oidc::{OidcProviderConfig, OidcSys};
pub(super) fn provider_config<'a>(oidc: &'a OidcSys, id: &str) -> Option<&'a OidcProviderConfig> {
oidc.get_provider_config(id)
}
pub(super) fn roles_claim_key(oidc: &OidcSys, provider_id: &str) -> Option<String> {
provider_config(oidc, 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);
}
}
@@ -0,0 +1,23 @@
// 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 crate::oidc::{OidcProviderSummary, OidcSys};
pub(super) fn list_providers(oidc: &OidcSys) -> Vec<OidcProviderSummary> {
oidc.list_providers()
}
pub(super) fn list_visible_providers(oidc: &OidcSys) -> Vec<OidcProviderSummary> {
oidc.list_visible_providers()
}
+79
View File
@@ -0,0 +1,79 @@
// 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::{claims, config, discovery, http};
use crate::{
federation::{FederatedAuthorization, FederatedCodeExchange, FederatedIdentityProvider, FederationError, Result},
oidc::{OidcProviderConfig, OidcProviderSummary, OidcSys},
};
use std::sync::Arc;
pub struct StandardOidcAdapter {
oidc: Arc<OidcSys>,
}
impl StandardOidcAdapter {
pub fn new(oidc: Arc<OidcSys>) -> Self {
Self { oidc }
}
}
#[async_trait::async_trait]
impl FederatedIdentityProvider for StandardOidcAdapter {
fn has_providers(&self) -> bool {
self.oidc.has_providers()
}
fn list_providers(&self) -> Vec<OidcProviderSummary> {
discovery::list_providers(&self.oidc)
}
fn list_visible_providers(&self) -> Vec<OidcProviderSummary> {
discovery::list_visible_providers(&self.oidc)
}
fn provider_config(&self, id: &str) -> Option<&OidcProviderConfig> {
config::provider_config(&self.oidc, id)
}
async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option<String>) -> Result<String> {
http::authorize_url(&self.oidc, provider_id, redirect_uri, redirect_after).await
}
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?;
Ok(FederatedCodeExchange {
authorization: claims::authorization(&self.oidc, provider_id, oidc_claims),
redirect_after: session.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))
}
async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result<String> {
http::create_logout_token(&self.oidc, provider_id, id_token).await
}
async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result<Option<String>> {
http::build_logout_url(&self.oidc, logout_token, post_logout_redirect_uri).await
}
}
+57
View File
@@ -0,0 +1,57 @@
// 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 crate::{
federation::{FederationError, Result},
oidc::{OidcClaims, OidcSys},
oidc_state::OidcAuthSession,
};
pub(super) async fn authorize_url(
oidc: &OidcSys,
provider_id: &str,
redirect_uri: &str,
redirect_after: Option<String>,
) -> Result<String> {
oidc.authorize_url(provider_id, redirect_uri, redirect_after)
.await
.map_err(FederationError::Authorization)
}
pub(super) async fn exchange_code(
oidc: &OidcSys,
state: &str,
code: &str,
redirect_uri: &str,
) -> Result<(OidcClaims, String, OidcAuthSession, String)> {
oidc.exchange_code(state, code, redirect_uri)
.await
.map_err(FederationError::CodeExchange)
}
pub(super) async fn create_logout_token(oidc: &OidcSys, provider_id: &str, id_token: &str) -> Result<String> {
oidc.create_logout_token(provider_id, id_token)
.await
.map_err(FederationError::Logout)
}
pub(super) async fn build_logout_url(
oidc: &OidcSys,
logout_token: &str,
post_logout_redirect_uri: &str,
) -> Result<Option<String>> {
oidc.build_logout_url(logout_token, post_logout_redirect_uri)
.await
.map_err(FederationError::Logout)
}
+21
View File
@@ -0,0 +1,21 @@
// 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.
mod claims;
mod config;
mod discovery;
mod flow;
mod http;
pub use flow::StandardOidcAdapter;