mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 21:46:50 +00:00
refactor(iam): introduce federated identity boundary (#5018)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user