mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 05:26:50 +00:00
refactor(iam): introduce federated identity boundary (#5018)
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
// 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::{FederatedSessionBindingError, FederatedSessionTransaction};
|
||||||
|
use rustfs_credentials::Credentials;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait FederatedSessionBinding: Send + Sync {
|
||||||
|
async fn bind(
|
||||||
|
&self,
|
||||||
|
transaction: &FederatedSessionTransaction,
|
||||||
|
) -> core::result::Result<Credentials, FederatedSessionBindingError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, FederationError>;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum FederatedSessionBindingError {
|
||||||
|
#[error("{0}")]
|
||||||
|
InvalidRequest(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum FederationError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Authorization(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
CodeExchange(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
TokenVerification(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
Logout(String),
|
||||||
|
#[error("no policies are available for this OIDC token")]
|
||||||
|
NoAuthorizationContext,
|
||||||
|
#[error(transparent)]
|
||||||
|
Binding(#[from] FederatedSessionBindingError),
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// 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 binding;
|
||||||
|
mod error;
|
||||||
|
mod model;
|
||||||
|
pub mod oidc;
|
||||||
|
mod provider;
|
||||||
|
mod registry;
|
||||||
|
mod transaction;
|
||||||
|
|
||||||
|
pub use binding::FederatedSessionBinding;
|
||||||
|
pub use error::{FederatedSessionBindingError, FederationError, Result};
|
||||||
|
pub use model::{
|
||||||
|
FederatedAuthorization, FederatedClaims, FederatedCodeExchange, FederatedLoginSession, FederatedSession,
|
||||||
|
FederatedSessionTransaction,
|
||||||
|
};
|
||||||
|
pub use provider::FederatedIdentityProvider;
|
||||||
|
pub use registry::FederatedIdentityRegistry;
|
||||||
|
pub use transaction::FederatedIdentityService;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// 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 rustfs_credentials::Credentials;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FederatedClaims {
|
||||||
|
pub sub: String,
|
||||||
|
pub email: String,
|
||||||
|
pub username: String,
|
||||||
|
pub groups: Vec<String>,
|
||||||
|
pub raw: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FederatedClaims {
|
||||||
|
pub fn session_identity(&self) -> String {
|
||||||
|
if !self.username.is_empty() {
|
||||||
|
self.username.clone()
|
||||||
|
} else if !self.email.is_empty() {
|
||||||
|
self.email.clone()
|
||||||
|
} else if !self.sub.is_empty() {
|
||||||
|
self.sub.clone()
|
||||||
|
} else {
|
||||||
|
"oidc-user-unknown".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FederatedAuthorization {
|
||||||
|
pub provider_id: String,
|
||||||
|
pub claims: FederatedClaims,
|
||||||
|
pub policies: Vec<String>,
|
||||||
|
pub groups: Vec<String>,
|
||||||
|
pub roles_claim_key: Option<String>,
|
||||||
|
pub roles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FederatedAuthorization {
|
||||||
|
pub fn has_authorization_context(&self) -> bool {
|
||||||
|
!self.policies.is_empty() || !self.groups.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FederatedCodeExchange {
|
||||||
|
pub authorization: FederatedAuthorization,
|
||||||
|
pub redirect_after: Option<String>,
|
||||||
|
pub id_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FederatedSessionTransaction {
|
||||||
|
pub authorization: FederatedAuthorization,
|
||||||
|
pub duration_seconds: usize,
|
||||||
|
pub session_policy: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FederatedSession {
|
||||||
|
pub credentials: Credentials,
|
||||||
|
pub authorization: FederatedAuthorization,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FederatedLoginSession {
|
||||||
|
pub session: FederatedSession,
|
||||||
|
pub redirect_after: Option<String>,
|
||||||
|
pub logout_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn claims(username: &str, email: &str, sub: &str) -> FederatedClaims {
|
||||||
|
FederatedClaims {
|
||||||
|
sub: sub.to_string(),
|
||||||
|
email: email.to_string(),
|
||||||
|
username: username.to_string(),
|
||||||
|
groups: Vec::new(),
|
||||||
|
raw: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorization(policies: Vec<String>, groups: Vec<String>) -> FederatedAuthorization {
|
||||||
|
FederatedAuthorization {
|
||||||
|
provider_id: "standard_oidc".to_string(),
|
||||||
|
claims: claims("", "", "subject"),
|
||||||
|
policies,
|
||||||
|
groups,
|
||||||
|
roles_claim_key: None,
|
||||||
|
roles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_identity_preserves_existing_fallback_order() {
|
||||||
|
assert_eq!(claims("john", "john@example.com", "sub-1").session_identity(), "john");
|
||||||
|
assert_eq!(claims("", "john@example.com", "sub-1").session_identity(), "john@example.com");
|
||||||
|
assert_eq!(claims("", "", "sub-1").session_identity(), "sub-1");
|
||||||
|
assert_eq!(claims("", "", "").session_identity(), "oidc-user-unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authorization_context_accepts_policy_or_group() {
|
||||||
|
assert!(!authorization(Vec::new(), Vec::new()).has_authorization_context());
|
||||||
|
assert!(authorization(vec!["consoleAdmin".to_string()], Vec::new()).has_authorization_context());
|
||||||
|
assert!(authorization(Vec::new(), vec!["RustFS.ConsoleAdmin".to_string()]).has_authorization_context());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 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, FederatedCodeExchange, Result};
|
||||||
|
use crate::oidc::{OidcProviderConfig, OidcProviderSummary};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait FederatedIdentityProvider: Send + Sync {
|
||||||
|
fn has_providers(&self) -> bool;
|
||||||
|
|
||||||
|
fn list_providers(&self) -> Vec<OidcProviderSummary>;
|
||||||
|
|
||||||
|
fn list_visible_providers(&self) -> Vec<OidcProviderSummary>;
|
||||||
|
|
||||||
|
fn provider_config(&self, id: &str) -> Option<&OidcProviderConfig>;
|
||||||
|
|
||||||
|
async fn authorize_url(&self, provider_id: &str, redirect_uri: &str, redirect_after: Option<String>) -> Result<String>;
|
||||||
|
|
||||||
|
async fn exchange_code(&self, state: &str, code: &str, redirect_uri: &str) -> Result<FederatedCodeExchange>;
|
||||||
|
|
||||||
|
async fn verify_web_identity_token(&self, jwt: &str) -> Result<FederatedAuthorization>;
|
||||||
|
|
||||||
|
async fn create_logout_token(&self, provider_id: &str, id_token: &str) -> Result<String>;
|
||||||
|
|
||||||
|
async fn build_logout_url(&self, logout_token: &str, post_logout_redirect_uri: &str) -> Result<Option<String>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// 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::FederatedIdentityProvider;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Immutable registry of built-in federation adapters.
|
||||||
|
pub struct FederatedIdentityRegistry {
|
||||||
|
standard_oidc: Arc<dyn FederatedIdentityProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FederatedIdentityRegistry {
|
||||||
|
pub fn new(standard_oidc: Arc<dyn FederatedIdentityProvider>) -> Self {
|
||||||
|
Self { standard_oidc }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn standard_oidc(&self) -> &dyn FederatedIdentityProvider {
|
||||||
|
self.standard_oidc.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
// 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::{
|
||||||
|
FederatedIdentityRegistry, FederatedLoginSession, FederatedSession, FederatedSessionBinding, FederatedSessionTransaction,
|
||||||
|
FederationError, Result,
|
||||||
|
};
|
||||||
|
use crate::oidc::{OidcProviderConfig, OidcProviderSummary};
|
||||||
|
|
||||||
|
pub struct FederatedIdentityService {
|
||||||
|
registry: FederatedIdentityRegistry,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FederatedIdentityService {
|
||||||
|
pub fn new(registry: FederatedIdentityRegistry) -> Self {
|
||||||
|
Self { registry }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_providers(&self) -> bool {
|
||||||
|
self.registry.standard_oidc().has_providers()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_providers(&self) -> Vec<OidcProviderSummary> {
|
||||||
|
self.registry.standard_oidc().list_providers()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_visible_providers(&self) -> Vec<OidcProviderSummary> {
|
||||||
|
self.registry.standard_oidc().list_visible_providers()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_provider_config(&self, id: &str) -> Option<&OidcProviderConfig> {
|
||||||
|
self.registry.standard_oidc().provider_config(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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn complete_authorization_code(
|
||||||
|
&self,
|
||||||
|
state: &str,
|
||||||
|
code: &str,
|
||||||
|
redirect_uri: &str,
|
||||||
|
duration_seconds: usize,
|
||||||
|
binding: &dyn FederatedSessionBinding,
|
||||||
|
) -> Result<FederatedLoginSession> {
|
||||||
|
let exchange = self.registry.standard_oidc().exchange_code(state, code, redirect_uri).await?;
|
||||||
|
let provider_id = exchange.authorization.provider_id.clone();
|
||||||
|
let transaction = FederatedSessionTransaction {
|
||||||
|
authorization: exchange.authorization,
|
||||||
|
duration_seconds,
|
||||||
|
session_policy: None,
|
||||||
|
};
|
||||||
|
let credentials = binding.bind(&transaction).await?;
|
||||||
|
let logout_token = self
|
||||||
|
.registry
|
||||||
|
.standard_oidc()
|
||||||
|
.create_logout_token(&provider_id, &exchange.id_token)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(FederatedLoginSession {
|
||||||
|
session: FederatedSession {
|
||||||
|
credentials,
|
||||||
|
authorization: transaction.authorization,
|
||||||
|
},
|
||||||
|
redirect_after: exchange.redirect_after,
|
||||||
|
logout_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assume_role_with_web_identity(
|
||||||
|
&self,
|
||||||
|
jwt: &str,
|
||||||
|
duration_seconds: usize,
|
||||||
|
session_policy: Option<String>,
|
||||||
|
binding: &dyn FederatedSessionBinding,
|
||||||
|
) -> Result<FederatedSession> {
|
||||||
|
let authorization = self.registry.standard_oidc().verify_web_identity_token(jwt).await?;
|
||||||
|
if !authorization.has_authorization_context() {
|
||||||
|
tracing::warn!(
|
||||||
|
provider_id = %authorization.provider_id,
|
||||||
|
username = %authorization.claims.username,
|
||||||
|
sub = %authorization.claims.sub,
|
||||||
|
policy_count = authorization.policies.len(),
|
||||||
|
group_count = authorization.groups.len(),
|
||||||
|
"AssumeRoleWithWebIdentity has no mapped policies or groups"
|
||||||
|
);
|
||||||
|
return Err(FederationError::NoAuthorizationContext);
|
||||||
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
provider_id = %authorization.provider_id,
|
||||||
|
username = %authorization.claims.username,
|
||||||
|
policy_count = authorization.policies.len(),
|
||||||
|
group_count = authorization.groups.len(),
|
||||||
|
policies = ?authorization.policies,
|
||||||
|
groups = ?authorization.groups,
|
||||||
|
"AssumeRoleWithWebIdentity mapped OIDC policies and groups"
|
||||||
|
);
|
||||||
|
|
||||||
|
let transaction = FederatedSessionTransaction {
|
||||||
|
authorization,
|
||||||
|
duration_seconds,
|
||||||
|
session_policy,
|
||||||
|
};
|
||||||
|
let credentials = binding.bind(&transaction).await?;
|
||||||
|
|
||||||
|
Ok(FederatedSession {
|
||||||
|
credentials,
|
||||||
|
authorization: transaction.authorization,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::federation::{
|
||||||
|
FederatedAuthorization, FederatedClaims, FederatedCodeExchange, FederatedIdentityProvider, FederatedSessionBindingError,
|
||||||
|
};
|
||||||
|
use crate::oidc::{OidcProviderConfig, OidcProviderSummary};
|
||||||
|
use rustfs_credentials::Credentials;
|
||||||
|
use std::sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TestProvider {
|
||||||
|
with_policy: bool,
|
||||||
|
events: Arc<Mutex<Vec<&'static str>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestProvider {
|
||||||
|
fn record(&self, event: &'static str) {
|
||||||
|
self.events.lock().expect("event log should not be poisoned").push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorization(&self) -> FederatedAuthorization {
|
||||||
|
FederatedAuthorization {
|
||||||
|
provider_id: "default".to_string(),
|
||||||
|
claims: FederatedClaims {
|
||||||
|
sub: "subject".to_string(),
|
||||||
|
email: String::new(),
|
||||||
|
username: "user".to_string(),
|
||||||
|
groups: Vec::new(),
|
||||||
|
raw: Default::default(),
|
||||||
|
},
|
||||||
|
policies: if self.with_policy {
|
||||||
|
vec!["readwrite".to_string()]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
},
|
||||||
|
groups: Vec::new(),
|
||||||
|
roles_claim_key: None,
|
||||||
|
roles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FederatedIdentityProvider for TestProvider {
|
||||||
|
fn has_providers(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_providers(&self) -> Vec<OidcProviderSummary> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_visible_providers(&self) -> Vec<OidcProviderSummary> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_config(&self, _id: &str) -> Option<&OidcProviderConfig> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authorize_url(
|
||||||
|
&self,
|
||||||
|
_provider_id: &str,
|
||||||
|
_redirect_uri: &str,
|
||||||
|
_redirect_after: Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
Ok("https://identity.example/authorize".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_code(&self, _state: &str, _code: &str, _redirect_uri: &str) -> Result<FederatedCodeExchange> {
|
||||||
|
self.record("exchange");
|
||||||
|
Ok(FederatedCodeExchange {
|
||||||
|
authorization: self.authorization(),
|
||||||
|
redirect_after: Some("/browser".to_string()),
|
||||||
|
id_token: "id-token".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_web_identity_token(&self, _jwt: &str) -> Result<FederatedAuthorization> {
|
||||||
|
self.record("verify");
|
||||||
|
Ok(self.authorization())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_logout_token(&self, _provider_id: &str, _id_token: &str) -> Result<String> {
|
||||||
|
self.record("logout");
|
||||||
|
Ok("logout-token".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_logout_url(&self, _logout_token: &str, _post_logout_redirect_uri: &str) -> Result<Option<String>> {
|
||||||
|
Ok(Some("https://identity.example/logout".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CountingBinding {
|
||||||
|
calls: AtomicUsize,
|
||||||
|
events: Arc<Mutex<Vec<&'static str>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FederatedSessionBinding for CountingBinding {
|
||||||
|
async fn bind(
|
||||||
|
&self,
|
||||||
|
transaction: &FederatedSessionTransaction,
|
||||||
|
) -> core::result::Result<Credentials, FederatedSessionBindingError> {
|
||||||
|
self.calls.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.events.lock().expect("event log should not be poisoned").push("bind");
|
||||||
|
Ok(Credentials {
|
||||||
|
access_key: transaction.authorization.claims.session_identity(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn callback_and_web_identity_share_session_binding() {
|
||||||
|
let events = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let provider = Arc::new(TestProvider {
|
||||||
|
with_policy: true,
|
||||||
|
events: events.clone(),
|
||||||
|
});
|
||||||
|
let binding = Arc::new(CountingBinding {
|
||||||
|
calls: AtomicUsize::new(0),
|
||||||
|
events: events.clone(),
|
||||||
|
});
|
||||||
|
let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(provider));
|
||||||
|
|
||||||
|
let login = service
|
||||||
|
.complete_authorization_code("state", "code", "https://console.example/callback", 3600, binding.as_ref())
|
||||||
|
.await
|
||||||
|
.expect("callback flow should complete");
|
||||||
|
assert_eq!(login.session.credentials.access_key, "user");
|
||||||
|
assert_eq!(login.redirect_after.as_deref(), Some("/browser"));
|
||||||
|
assert_eq!(login.logout_token, "logout-token");
|
||||||
|
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", 3600, None, binding.as_ref())
|
||||||
|
.await
|
||||||
|
.expect("web identity flow should complete");
|
||||||
|
assert_eq!(web_identity.credentials.access_key, "user");
|
||||||
|
assert_eq!(binding.calls.load(Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(events.lock().expect("event log should not be poisoned").as_slice(), ["verify", "bind"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn web_identity_without_policy_or_group_is_not_bound() {
|
||||||
|
let events = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let provider = Arc::new(TestProvider {
|
||||||
|
with_policy: false,
|
||||||
|
events: events.clone(),
|
||||||
|
});
|
||||||
|
let binding = Arc::new(CountingBinding {
|
||||||
|
calls: AtomicUsize::new(0),
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
let service = FederatedIdentityService::new(FederatedIdentityRegistry::new(provider));
|
||||||
|
|
||||||
|
let error = service
|
||||||
|
.assume_role_with_web_identity("jwt", 3600, None, binding.as_ref())
|
||||||
|
.await
|
||||||
|
.expect_err("authorization context is required");
|
||||||
|
|
||||||
|
assert!(matches!(error, FederationError::NoAuthorizationContext));
|
||||||
|
assert_eq!(binding.calls.load(Ordering::Relaxed), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ const EVENT_OIDC_STATE: &str = "oidc_state";
|
|||||||
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod federation;
|
||||||
pub mod keyring;
|
pub mod keyring;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod oidc;
|
pub mod oidc;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::admin::runtime_sources::{current_oidc_handle, default_admin_usecase};
|
use crate::admin::runtime_sources::{current_federated_identity_service, default_admin_usecase};
|
||||||
use crate::admin::storage_api::access::RequestContext;
|
use crate::admin::storage_api::access::RequestContext;
|
||||||
use crate::license::has_valid_license;
|
use crate::license::has_valid_license;
|
||||||
use crate::server::has_path_prefix;
|
use crate::server::has_path_prefix;
|
||||||
@@ -139,9 +139,10 @@ impl Config {
|
|||||||
let http_prefix = rustfs_config::RUSTFS_HTTP_PREFIX;
|
let http_prefix = rustfs_config::RUSTFS_HTTP_PREFIX;
|
||||||
|
|
||||||
// Collect OIDC provider info if available
|
// Collect OIDC provider info if available
|
||||||
let oidc = current_oidc_handle()
|
let oidc = current_federated_identity_service()
|
||||||
.map(|sys| {
|
.map(|federation| {
|
||||||
sys.list_visible_providers()
|
federation
|
||||||
|
.list_visible_providers()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| OidcProviderInfo {
|
.map(|p| OidcProviderInfo {
|
||||||
provider_id: p.provider_id,
|
provider_id: p.provider_id,
|
||||||
|
|||||||
@@ -12,12 +12,13 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use super::sts::create_oidc_sts_credentials;
|
|
||||||
use crate::admin::auth::validate_admin_request;
|
use crate::admin::auth::validate_admin_request;
|
||||||
use crate::admin::router::{AdminOperation, Operation, S3Router};
|
use crate::admin::router::{AdminOperation, Operation, S3Router};
|
||||||
use crate::admin::runtime_sources::{
|
use crate::admin::runtime_sources::{
|
||||||
current_app_context, current_object_store_handle_for_context, current_oidc_handle, current_server_config_for_context,
|
current_app_context, current_federated_identity_service, current_object_store_handle_for_context,
|
||||||
|
current_server_config_for_context,
|
||||||
};
|
};
|
||||||
|
use crate::admin::service::federated_identity::DefaultFederatedSessionBinding;
|
||||||
use crate::admin::storage_api::config::{read_admin_config_without_migrate, save_admin_server_config};
|
use crate::admin::storage_api::config::{read_admin_config_without_migrate, save_admin_server_config};
|
||||||
use crate::auth::{check_key_valid, get_session_token};
|
use crate::auth::{check_key_valid, get_session_token};
|
||||||
use crate::server::{ADMIN_PREFIX, CONSOLE_PREFIX, MINIO_ADMIN_PREFIX, RemoteAddr};
|
use crate::server::{ADMIN_PREFIX, CONSOLE_PREFIX, MINIO_ADMIN_PREFIX, RemoteAddr};
|
||||||
@@ -32,6 +33,7 @@ use rustfs_config::oidc::{
|
|||||||
};
|
};
|
||||||
use rustfs_config::server_config::Config as ServerConfig;
|
use rustfs_config::server_config::Config as ServerConfig;
|
||||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_RUSTFS_BROWSER_REDIRECT_URL, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_RUSTFS_BROWSER_REDIRECT_URL, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
||||||
|
use rustfs_iam::federation::{FederatedSessionBindingError, FederationError};
|
||||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||||
use rustfs_utils::egress::validate_outbound_url;
|
use rustfs_utils::egress::validate_outbound_url;
|
||||||
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
||||||
@@ -54,6 +56,24 @@ const CONSOLE_LOGIN_SUFFIX: &str = "/auth/login";
|
|||||||
const OIDC_STATE_LB_HINT: &str =
|
const OIDC_STATE_LB_HINT: &str =
|
||||||
"check load balancer session affinity for OIDC authorize/callback requests or configure RUSTFS_BROWSER_REDIRECT_URL";
|
"check load balancer session affinity for OIDC authorize/callback requests or configure RUSTFS_BROWSER_REDIRECT_URL";
|
||||||
|
|
||||||
|
fn callback_federation_error(error: FederationError) -> S3Error {
|
||||||
|
match error {
|
||||||
|
FederationError::CodeExchange(message) => {
|
||||||
|
S3Error::with_message(S3ErrorCode::AccessDenied, format!("code exchange failed: {message}"))
|
||||||
|
}
|
||||||
|
FederationError::Logout(message) => {
|
||||||
|
S3Error::with_message(S3ErrorCode::InternalError, format!("logout session creation failed: {message}"))
|
||||||
|
}
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::InvalidRequest(message)) => {
|
||||||
|
S3Error::with_message(S3ErrorCode::InvalidRequest, message)
|
||||||
|
}
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::Internal(message)) => {
|
||||||
|
S3Error::with_message(S3ErrorCode::InternalError, message)
|
||||||
|
}
|
||||||
|
other => S3Error::with_message(S3ErrorCode::InternalError, other.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate that a provider ID contains only safe characters (alphanumeric, underscore, hyphen).
|
/// Validate that a provider ID contains only safe characters (alphanumeric, underscore, hyphen).
|
||||||
fn is_valid_provider_id(id: &str) -> bool {
|
fn is_valid_provider_id(id: &str) -> bool {
|
||||||
!id.is_empty() && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
!id.is_empty() && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||||
@@ -274,9 +294,9 @@ pub struct ListOidcProvidersHandler {}
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Operation for ListOidcProvidersHandler {
|
impl Operation for ListOidcProvidersHandler {
|
||||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||||
let oidc_sys = current_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
let federation = current_federated_identity_service().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
||||||
|
|
||||||
let providers = oidc_sys.list_visible_providers();
|
let providers = federation.list_visible_providers();
|
||||||
let json_body = serde_json::to_vec(&providers)
|
let json_body = serde_json::to_vec(&providers)
|
||||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("serialize error: {e}")))?;
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("serialize error: {e}")))?;
|
||||||
|
|
||||||
@@ -445,7 +465,7 @@ impl Operation for OidcAuthorizeHandler {
|
|||||||
return Err(s3_error!(InvalidRequest, "invalid provider_id"));
|
return Err(s3_error!(InvalidRequest, "invalid provider_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let oidc_sys = current_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
let federation = current_federated_identity_service().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
||||||
|
|
||||||
// Derive the callback redirect URI from the request
|
// Derive the callback redirect URI from the request
|
||||||
let redirect_uri = derive_callback_uri(&req, provider_id)?;
|
let redirect_uri = derive_callback_uri(&req, provider_id)?;
|
||||||
@@ -454,7 +474,7 @@ impl Operation for OidcAuthorizeHandler {
|
|||||||
let redirect_after = extract_safe_redirect_after(&req.uri)?;
|
let redirect_after = extract_safe_redirect_after(&req.uri)?;
|
||||||
let redirect_after_log = redirect_after.clone();
|
let redirect_after_log = redirect_after.clone();
|
||||||
|
|
||||||
let auth_url = oidc_sys
|
let auth_url = federation
|
||||||
.authorize_url(provider_id, &redirect_uri, redirect_after)
|
.authorize_url(provider_id, &redirect_uri, redirect_after)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -535,35 +555,40 @@ impl Operation for OidcCallbackHandler {
|
|||||||
let state =
|
let state =
|
||||||
extract_query_param(&req.uri, "state").ok_or_else(|| s3_error!(InvalidRequest, "missing 'state' query parameter"))?;
|
extract_query_param(&req.uri, "state").ok_or_else(|| s3_error!(InvalidRequest, "missing 'state' query parameter"))?;
|
||||||
|
|
||||||
let oidc_sys = current_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
let federation = current_federated_identity_service().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
||||||
|
|
||||||
let redirect_uri = derive_callback_uri(&req, provider_id)?;
|
let redirect_uri = derive_callback_uri(&req, provider_id)?;
|
||||||
|
|
||||||
// Exchange authorization code for tokens and extract claims
|
let login = federation
|
||||||
let (claims, actual_provider_id, session, id_token) =
|
.complete_authorization_code(&state, &code, &redirect_uri, 3600, &DefaultFederatedSessionBinding)
|
||||||
oidc_sys.exchange_code(&state, &code, &redirect_uri).await.map_err(|e| {
|
.await
|
||||||
let lb_hint = if is_invalid_oidc_state_error(&e) {
|
.map_err(|error| {
|
||||||
OIDC_STATE_LB_HINT
|
if let FederationError::CodeExchange(message) = &error {
|
||||||
} else {
|
let lb_hint = if is_invalid_oidc_state_error(message) {
|
||||||
""
|
OIDC_STATE_LB_HINT
|
||||||
};
|
} else {
|
||||||
error!(
|
""
|
||||||
event = EVENT_ADMIN_OIDC_STATE,
|
};
|
||||||
component = LOG_COMPONENT_ADMIN,
|
error!(
|
||||||
subsystem = LOG_SUBSYSTEM_OIDC,
|
event = EVENT_ADMIN_OIDC_STATE,
|
||||||
result = "code_exchange_failed",
|
component = LOG_COMPONENT_ADMIN,
|
||||||
requested_provider_id = %provider_id,
|
subsystem = LOG_SUBSYSTEM_OIDC,
|
||||||
redirect_uri = %redirect_uri,
|
result = "code_exchange_failed",
|
||||||
code = %code,
|
requested_provider_id = %provider_id,
|
||||||
state = %state,
|
redirect_uri = %redirect_uri,
|
||||||
code_len = code.len(),
|
code = %code,
|
||||||
state_len = state.len(),
|
state = %state,
|
||||||
error = %e,
|
code_len = code.len(),
|
||||||
lb_hint = %lb_hint,
|
state_len = state.len(),
|
||||||
"admin oidc state"
|
error = %message,
|
||||||
);
|
lb_hint = %lb_hint,
|
||||||
S3Error::with_message(S3ErrorCode::AccessDenied, format!("code exchange failed: {e}"))
|
"admin oidc state"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
callback_federation_error(error)
|
||||||
})?;
|
})?;
|
||||||
|
let authorization = &login.session.authorization;
|
||||||
|
let actual_provider_id = &authorization.provider_id;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
event = EVENT_ADMIN_OIDC_STATE,
|
event = EVENT_ADMIN_OIDC_STATE,
|
||||||
@@ -574,32 +599,20 @@ impl Operation for OidcCallbackHandler {
|
|||||||
"admin oidc state"
|
"admin oidc state"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Map claims to policies and groups
|
|
||||||
let (policies, groups) = oidc_sys.map_claims_to_policies(&actual_provider_id, &claims);
|
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
event = EVENT_ADMIN_OIDC_STATE,
|
event = EVENT_ADMIN_OIDC_STATE,
|
||||||
component = LOG_COMPONENT_ADMIN,
|
component = LOG_COMPONENT_ADMIN,
|
||||||
subsystem = LOG_SUBSYSTEM_OIDC,
|
subsystem = LOG_SUBSYSTEM_OIDC,
|
||||||
provider_id = %actual_provider_id,
|
provider_id = %actual_provider_id,
|
||||||
policy_count = policies.len(),
|
policy_count = authorization.policies.len(),
|
||||||
group_count = groups.len(),
|
group_count = authorization.groups.len(),
|
||||||
policies = ?policies,
|
policies = ?authorization.policies,
|
||||||
groups = ?groups,
|
groups = ?authorization.groups,
|
||||||
state = "claims_mapped",
|
state = "claims_mapped",
|
||||||
"admin oidc state"
|
"admin oidc state"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate STS credentials using the shared helper.
|
let new_cred = &login.session.credentials;
|
||||||
// Console/OIDC sessions use a fixed 1-hour duration as a security/UX choice.
|
|
||||||
// Longer-lived credentials (15 min to 12 hours) can be requested via CLI/SDK
|
|
||||||
// through AssumeRoleWithWebIdentity.
|
|
||||||
let new_cred = create_oidc_sts_credentials(&claims, &actual_provider_id, &policies, &groups, 3600, None).await?;
|
|
||||||
|
|
||||||
let logout_token = oidc_sys
|
|
||||||
.create_logout_token(&actual_provider_id, &id_token)
|
|
||||||
.await
|
|
||||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("logout session creation failed: {e}")))?;
|
|
||||||
|
|
||||||
// Build redirect URL to console with credentials in the fragment
|
// Build redirect URL to console with credentials in the fragment
|
||||||
let console_redirect = build_console_redirect(
|
let console_redirect = build_console_redirect(
|
||||||
@@ -608,8 +621,8 @@ impl Operation for OidcCallbackHandler {
|
|||||||
&new_cred.secret_key,
|
&new_cred.secret_key,
|
||||||
&new_cred.session_token,
|
&new_cred.session_token,
|
||||||
new_cred.expiration,
|
new_cred.expiration,
|
||||||
session.redirect_after.as_deref(),
|
login.redirect_after.as_deref(),
|
||||||
Some(logout_token.as_str()),
|
Some(login.logout_token.as_str()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut resp = S3Response::new((StatusCode::FOUND, Body::empty()));
|
let mut resp = S3Response::new((StatusCode::FOUND, Body::empty()));
|
||||||
@@ -636,8 +649,8 @@ impl Operation for OidcLogoutHandler {
|
|||||||
return redirect_response(&fallback_location);
|
return redirect_response(&fallback_location);
|
||||||
};
|
};
|
||||||
|
|
||||||
let location = match current_oidc_handle() {
|
let location = match current_federated_identity_service() {
|
||||||
Some(oidc_sys) => match oidc_sys.build_logout_url(&logout_token, &fallback_location).await {
|
Some(federation) => match federation.build_logout_url(&logout_token, &fallback_location).await {
|
||||||
Ok(Some(url)) => url,
|
Ok(Some(url)) => url,
|
||||||
Ok(None) => fallback_location.clone(),
|
Ok(None) => fallback_location.clone(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -664,8 +677,8 @@ impl Operation for OidcLogoutHandler {
|
|||||||
/// from request headers. For production deployments behind a reverse proxy, configuring
|
/// from request headers. For production deployments behind a reverse proxy, configuring
|
||||||
/// an explicit redirect_uri is recommended to prevent header manipulation.
|
/// an explicit redirect_uri is recommended to prevent header manipulation.
|
||||||
fn derive_callback_uri(req: &S3Request<Body>, provider_id: &str) -> S3Result<String> {
|
fn derive_callback_uri(req: &S3Request<Body>, provider_id: &str) -> S3Result<String> {
|
||||||
if let Some(oidc_sys) = current_oidc_handle()
|
if let Some(federation) = current_federated_identity_service()
|
||||||
&& let Some(config) = oidc_sys.get_provider_config(provider_id)
|
&& let Some(config) = federation.get_provider_config(provider_id)
|
||||||
{
|
{
|
||||||
return derive_callback_uri_with_provider_config(req, provider_id, Some(config));
|
return derive_callback_uri_with_provider_config(req, provider_id, Some(config));
|
||||||
}
|
}
|
||||||
@@ -1326,6 +1339,38 @@ mod tests {
|
|||||||
assert_eq!(extract_query_param(&uri, "missing"), None);
|
assert_eq!(extract_query_param(&uri, "missing"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn callback_errors_preserve_existing_s3_semantics() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
FederationError::CodeExchange("invalid state".to_string()),
|
||||||
|
S3ErrorCode::AccessDenied,
|
||||||
|
"code exchange failed: invalid state",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FederationError::Logout("session unavailable".to_string()),
|
||||||
|
S3ErrorCode::InternalError,
|
||||||
|
"logout session creation failed: session unavailable",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::InvalidRequest("invalid policy".to_string())),
|
||||||
|
S3ErrorCode::InvalidRequest,
|
||||||
|
"invalid policy",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::Internal("failed to store temp user".to_string())),
|
||||||
|
S3ErrorCode::InternalError,
|
||||||
|
"failed to store temp user",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (error, expected_code, expected_message) in cases {
|
||||||
|
let error = callback_federation_error(error);
|
||||||
|
assert_eq!(error.code(), &expected_code);
|
||||||
|
assert_eq!(error.message(), Some(expected_message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extract_query_param_empty() {
|
fn test_extract_query_param_empty() {
|
||||||
let uri: http::Uri = "http://localhost/callback".parse().unwrap();
|
let uri: http::Uri = "http://localhost/callback".parse().unwrap();
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
use crate::admin::auth::validate_admin_request;
|
use crate::admin::auth::validate_admin_request;
|
||||||
use crate::admin::router::{AdminOperation, Operation, S3Router};
|
use crate::admin::router::{AdminOperation, Operation, S3Router};
|
||||||
use crate::admin::runtime_sources::{
|
use crate::admin::runtime_sources::{
|
||||||
current_deployment_id, current_endpoints_handle, current_iam_handle, current_object_store_handle, current_oidc_handle,
|
current_deployment_id, current_endpoints_handle, current_federated_identity_service, current_iam_handle,
|
||||||
current_outbound_tls_generation, current_outbound_tls_state, current_region, current_replication_pool_handle,
|
current_object_store_handle, current_outbound_tls_generation, current_outbound_tls_state, current_region,
|
||||||
current_replication_stats_handle, current_runtime_port, current_server_config, current_token_signing_key,
|
current_replication_pool_handle, current_replication_stats_handle, current_runtime_port, current_server_config,
|
||||||
object_store_from_req,
|
current_token_signing_key, object_store_from_req,
|
||||||
};
|
};
|
||||||
use crate::admin::site_replication_identity::{
|
use crate::admin::site_replication_identity::{
|
||||||
canonical_endpoint, deployment_id_for_endpoint, normalize_peer_map_by_identity_with, same_identity_endpoint,
|
canonical_endpoint, deployment_id_for_endpoint, normalize_peer_map_by_identity_with, same_identity_endpoint,
|
||||||
@@ -2987,13 +2987,13 @@ async fn build_sr_info(state: &SiteReplicationState, local_peer: &PeerInfo) -> S
|
|||||||
|
|
||||||
fn local_idp_settings() -> IDPSettings {
|
fn local_idp_settings() -> IDPSettings {
|
||||||
let mut settings = IDPSettings::default();
|
let mut settings = IDPSettings::default();
|
||||||
if let Some(oidc) = current_oidc_handle() {
|
if let Some(federation) = current_federated_identity_service() {
|
||||||
let providers = oidc.list_providers();
|
let providers = federation.list_providers();
|
||||||
settings.open_id.enabled = !providers.is_empty();
|
settings.open_id.enabled = !providers.is_empty();
|
||||||
settings.open_id.region = current_region().map(|region| region.to_string()).unwrap_or_default();
|
settings.open_id.region = current_region().map(|region| region.to_string()).unwrap_or_default();
|
||||||
|
|
||||||
for provider in providers {
|
for provider in providers {
|
||||||
let Some(config) = oidc.get_provider_config(&provider.provider_id) else {
|
let Some(config) = federation.get_provider_config(&provider.provider_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let provider_settings = OpenIDProviderSettings {
|
let provider_settings = OpenIDProviderSettings {
|
||||||
|
|||||||
@@ -13,14 +13,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use super::is_admin::IsAdminHandler;
|
use super::is_admin::IsAdminHandler;
|
||||||
|
use crate::admin::service::federated_identity::DefaultFederatedSessionBinding;
|
||||||
|
use crate::admin::service::session_policy::populate_session_policy;
|
||||||
use crate::admin::storage_api::bucket::utils::serialize;
|
use crate::admin::storage_api::bucket::utils::serialize;
|
||||||
use crate::{
|
use crate::{
|
||||||
admin::runtime_sources::{current_action_credentials, current_oidc_handle, current_token_signing_key},
|
admin::runtime_sources::{current_action_credentials, current_federated_identity_service, current_token_signing_key},
|
||||||
admin::{
|
admin::{
|
||||||
handlers::site_replication::site_replication_iam_change_hook,
|
handlers::site_replication::site_replication_iam_change_hook,
|
||||||
router::{AdminOperation, Operation, S3Router},
|
router::{AdminOperation, Operation, S3Router},
|
||||||
},
|
},
|
||||||
auth::{check_key_valid, extract_string_list_claim, get_session_token},
|
auth::{check_key_valid, get_session_token},
|
||||||
server::ADMIN_PREFIX,
|
server::ADMIN_PREFIX,
|
||||||
server::RemoteAddr,
|
server::RemoteAddr,
|
||||||
};
|
};
|
||||||
@@ -29,12 +31,12 @@ use http::header::HeaderValue;
|
|||||||
use hyper::Method;
|
use hyper::Method;
|
||||||
use matchit::Params;
|
use matchit::Params;
|
||||||
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
|
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
|
||||||
use rustfs_iam::{oidc::OidcClaims, sys::SESSION_POLICY_NAME};
|
use rustfs_iam::federation::{FederatedSessionBindingError, FederationError};
|
||||||
use rustfs_madmin::{SITE_REPL_API_VERSION, SRIAMItem, SRSTSCredential};
|
use rustfs_madmin::{SITE_REPL_API_VERSION, SRIAMItem, SRSTSCredential};
|
||||||
use rustfs_policy::{
|
use rustfs_policy::{
|
||||||
auth::get_new_credentials_with_metadata,
|
auth::get_new_credentials_with_metadata,
|
||||||
policy::{
|
policy::{
|
||||||
Args, Policy,
|
Args,
|
||||||
action::{Action, StsAction},
|
action::{Action, StsAction},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -46,7 +48,6 @@ use s3s::{
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serde_urlencoded::from_bytes;
|
use serde_urlencoded::from_bytes;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
@@ -75,138 +76,21 @@ fn clamp_assume_role_duration(duration_seconds: usize) -> usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_identity_authorization_context(policies: &[String], groups: &[String]) -> bool {
|
fn web_identity_federation_error(error: FederationError) -> S3Error {
|
||||||
!policies.is_empty() || !groups.is_empty()
|
match error {
|
||||||
}
|
FederationError::TokenVerification(message) => {
|
||||||
|
S3Error::with_message(S3ErrorCode::AccessDenied, format!("token verification failed: {message}"))
|
||||||
fn configured_roles_claim_key(provider_id: &str) -> Option<String> {
|
|
||||||
current_oidc_handle()
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|oidc_sys| oidc_sys.get_provider_config(provider_id))
|
|
||||||
.map(|cfg| cfg.roles_claim.trim().to_string())
|
|
||||||
.filter(|claim| !claim.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_oidc_token_claims(
|
|
||||||
claims: &OidcClaims,
|
|
||||||
provider_id: &str,
|
|
||||||
groups: &[String],
|
|
||||||
roles_claim_key: Option<&str>,
|
|
||||||
) -> HashMap<String, Value> {
|
|
||||||
let mut token_claims: HashMap<String, Value> = HashMap::new();
|
|
||||||
token_claims.insert("sub".to_string(), Value::String(claims.sub.clone()));
|
|
||||||
token_claims.insert("iss".to_string(), Value::String("rustfs-oidc".to_string()));
|
|
||||||
token_claims.insert("oidc_provider".to_string(), Value::String(provider_id.to_string()));
|
|
||||||
|
|
||||||
if !claims.email.is_empty() {
|
|
||||||
token_claims.insert("email".to_string(), Value::String(claims.email.clone()));
|
|
||||||
}
|
|
||||||
if !claims.username.is_empty() {
|
|
||||||
token_claims.insert("preferred_username".to_string(), Value::String(claims.username.clone()));
|
|
||||||
}
|
|
||||||
if !groups.is_empty() {
|
|
||||||
token_claims.insert(
|
|
||||||
"groups".to_string(),
|
|
||||||
Value::Array(groups.iter().map(|g| Value::String(g.clone())).collect()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(roles_claim_key) = roles_claim_key {
|
|
||||||
let roles = extract_string_list_claim(&claims.raw, roles_claim_key);
|
|
||||||
if !roles.is_empty() {
|
|
||||||
token_claims.insert("roles".to_string(), Value::Array(roles.into_iter().map(Value::String).collect()));
|
|
||||||
}
|
}
|
||||||
}
|
FederationError::NoAuthorizationContext => {
|
||||||
token_claims
|
s3_error!(InvalidArgument, "no policies are available for this OIDC token")
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_oidc_session_identity(claims: &OidcClaims) -> String {
|
|
||||||
if !claims.username.is_empty() {
|
|
||||||
claims.username.clone()
|
|
||||||
} else if !claims.email.is_empty() {
|
|
||||||
claims.email.clone()
|
|
||||||
} else if !claims.sub.is_empty() {
|
|
||||||
claims.sub.clone()
|
|
||||||
} else {
|
|
||||||
"oidc-user-unknown".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn log_oidc_policy_diagnostics(
|
|
||||||
iam_store: &rustfs_iam::sys::IamSys<rustfs_iam::store::object::ObjectStore>,
|
|
||||||
provider_id: &str,
|
|
||||||
parent_user: &str,
|
|
||||||
policies: &[String],
|
|
||||||
groups: &[String],
|
|
||||||
) {
|
|
||||||
if policies.is_empty() {
|
|
||||||
let policy_documents = BTreeMap::<String, Value>::new();
|
|
||||||
let missing_policies = Vec::<String>::new();
|
|
||||||
let combined_policy = Value::Null;
|
|
||||||
debug!(
|
|
||||||
provider_id = %provider_id,
|
|
||||||
parent_user = %parent_user,
|
|
||||||
policy_count = 0,
|
|
||||||
group_count = groups.len(),
|
|
||||||
policies = ?policies,
|
|
||||||
groups = ?groups,
|
|
||||||
policy_documents = ?policy_documents,
|
|
||||||
missing_policies = ?missing_policies,
|
|
||||||
combined_policy = ?combined_policy,
|
|
||||||
"OIDC STS policy diagnostics"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match iam_store.list_policy_docs("").await {
|
|
||||||
Ok(policy_docs) => {
|
|
||||||
let mut policy_documents = BTreeMap::new();
|
|
||||||
let mut missing_policies = Vec::new();
|
|
||||||
for policy_name in policies {
|
|
||||||
match policy_docs.get(policy_name) {
|
|
||||||
Some(policy_doc) => {
|
|
||||||
let policy_doc_json = serde_json::to_value(policy_doc).unwrap_or_else(|err| {
|
|
||||||
serde_json::json!({
|
|
||||||
"serialization_error": err.to_string(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
policy_documents.insert(policy_name.clone(), policy_doc_json);
|
|
||||||
}
|
|
||||||
None => missing_policies.push(policy_name.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let combined_policy = iam_store.get_combined_policy(policies).await;
|
|
||||||
let combined_policy_json = serde_json::to_value(&combined_policy).unwrap_or_else(|err| {
|
|
||||||
serde_json::json!({
|
|
||||||
"serialization_error": err.to_string(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
provider_id = %provider_id,
|
|
||||||
parent_user = %parent_user,
|
|
||||||
policy_count = policies.len(),
|
|
||||||
group_count = groups.len(),
|
|
||||||
policies = ?policies,
|
|
||||||
groups = ?groups,
|
|
||||||
missing_policies = ?missing_policies,
|
|
||||||
policy_documents = ?policy_documents,
|
|
||||||
combined_policy = ?combined_policy_json,
|
|
||||||
"OIDC STS policy diagnostics"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
FederationError::Binding(FederatedSessionBindingError::InvalidRequest(message)) => {
|
||||||
warn!(
|
S3Error::with_message(S3ErrorCode::InvalidRequest, message)
|
||||||
provider_id = %provider_id,
|
|
||||||
parent_user = %parent_user,
|
|
||||||
policy_count = policies.len(),
|
|
||||||
group_count = groups.len(),
|
|
||||||
policies = ?policies,
|
|
||||||
groups = ?groups,
|
|
||||||
error = %err,
|
|
||||||
"OIDC STS policy diagnostics failed"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::Internal(message)) => {
|
||||||
|
S3Error::with_message(S3ErrorCode::InternalError, message)
|
||||||
|
}
|
||||||
|
other => S3Error::with_message(S3ErrorCode::InternalError, other.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,42 +295,6 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu
|
|||||||
return Err(s3_error!(InvalidArgument, "not support version"));
|
return Err(s3_error!(InvalidArgument, "not support version"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the JWT and extract claims
|
|
||||||
let oidc_sys = current_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
|
||||||
|
|
||||||
let (claims, provider_id) = oidc_sys
|
|
||||||
.verify_web_identity_token(&body.web_identity_token)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
warn!("AssumeRoleWithWebIdentity JWT verification failed: {}", e);
|
|
||||||
S3Error::with_message(S3ErrorCode::AccessDenied, format!("token verification failed: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Map claims to policies and groups
|
|
||||||
let (policies, groups) = oidc_sys.map_claims_to_policies(&provider_id, &claims);
|
|
||||||
|
|
||||||
if !has_identity_authorization_context(&policies, &groups) {
|
|
||||||
warn!(
|
|
||||||
provider_id = %provider_id,
|
|
||||||
username = %claims.username,
|
|
||||||
sub = %claims.sub,
|
|
||||||
policy_count = policies.len(),
|
|
||||||
group_count = groups.len(),
|
|
||||||
"AssumeRoleWithWebIdentity has no mapped policies or groups"
|
|
||||||
);
|
|
||||||
return Err(s3_error!(InvalidArgument, "no policies are available for this OIDC token"));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
provider_id = %provider_id,
|
|
||||||
username = %claims.username,
|
|
||||||
policy_count = policies.len(),
|
|
||||||
group_count = groups.len(),
|
|
||||||
policies = ?policies,
|
|
||||||
groups = ?groups,
|
|
||||||
"AssumeRoleWithWebIdentity mapped OIDC policies and groups"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut duration = if body.duration_seconds > 0 {
|
let mut duration = if body.duration_seconds > 0 {
|
||||||
body.duration_seconds
|
body.duration_seconds
|
||||||
} else {
|
} else {
|
||||||
@@ -455,18 +303,24 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu
|
|||||||
|
|
||||||
// Enforce reasonable bounds for STS credentials duration (similar to AWS STS)
|
// Enforce reasonable bounds for STS credentials duration (similar to AWS STS)
|
||||||
duration = duration.clamp(900, 43200);
|
duration = duration.clamp(900, 43200);
|
||||||
// Generate STS credentials using the shared helper
|
let federation = current_federated_identity_service().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
|
||||||
let new_cred = create_oidc_sts_credentials(
|
let session = federation
|
||||||
&claims,
|
.assume_role_with_web_identity(
|
||||||
&provider_id,
|
&body.web_identity_token,
|
||||||
&policies,
|
duration,
|
||||||
&groups,
|
if body.policy.is_empty() { None } else { Some(body.policy) },
|
||||||
duration,
|
&DefaultFederatedSessionBinding,
|
||||||
if body.policy.is_empty() { None } else { Some(&body.policy) },
|
)
|
||||||
)
|
.await
|
||||||
.await?;
|
.map_err(|error| {
|
||||||
|
if let FederationError::TokenVerification(message) = &error {
|
||||||
let subject = resolve_oidc_session_identity(&claims);
|
warn!("AssumeRoleWithWebIdentity JWT verification failed: {}", message);
|
||||||
|
}
|
||||||
|
web_identity_federation_error(error)
|
||||||
|
})?;
|
||||||
|
let authorization = &session.authorization;
|
||||||
|
let new_cred = &session.credentials;
|
||||||
|
let subject = authorization.claims.session_identity();
|
||||||
|
|
||||||
// Build XML response (AssumeRoleWithWebIdentityResponse)
|
// Build XML response (AssumeRoleWithWebIdentityResponse)
|
||||||
let expiration = new_cred
|
let expiration = new_cred
|
||||||
@@ -502,121 +356,6 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu
|
|||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared helper to generate STS credentials from OIDC claims.
|
|
||||||
/// Used by both the OIDC callback handler and AssumeRoleWithWebIdentity.
|
|
||||||
pub async fn create_oidc_sts_credentials(
|
|
||||||
claims: &OidcClaims,
|
|
||||||
provider_id: &str,
|
|
||||||
policies: &[String],
|
|
||||||
groups: &[String],
|
|
||||||
duration_seconds: usize,
|
|
||||||
session_policy: Option<&str>,
|
|
||||||
) -> S3Result<rustfs_credentials::Credentials> {
|
|
||||||
let roles_claim_key = configured_roles_claim_key(provider_id);
|
|
||||||
let mut token_claims = build_oidc_token_claims(claims, provider_id, groups, roles_claim_key.as_deref());
|
|
||||||
|
|
||||||
// Set expiration
|
|
||||||
let exp = OffsetDateTime::now_utc().saturating_add(Duration::seconds(duration_seconds as i64));
|
|
||||||
token_claims.insert("exp".to_string(), Value::Number(serde_json::Number::from(exp.unix_timestamp())));
|
|
||||||
|
|
||||||
// Set the parent user: prefer username, then email, then sub
|
|
||||||
let parent_user = resolve_oidc_session_identity(claims);
|
|
||||||
debug!(
|
|
||||||
provider_id = %provider_id,
|
|
||||||
parent_user = %parent_user,
|
|
||||||
email = %claims.email,
|
|
||||||
username = %claims.username,
|
|
||||||
sub = %claims.sub,
|
|
||||||
policy_count = policies.len(),
|
|
||||||
group_count = groups.len(),
|
|
||||||
policies = ?policies,
|
|
||||||
groups = ?groups,
|
|
||||||
roles_claim_key = ?roles_claim_key,
|
|
||||||
has_session_policy = session_policy.is_some(),
|
|
||||||
"OIDC STS credential claims prepared"
|
|
||||||
);
|
|
||||||
token_claims.insert("parent".to_string(), Value::String(parent_user.clone()));
|
|
||||||
|
|
||||||
// Set policies as a comma-separated string
|
|
||||||
if !policies.is_empty() {
|
|
||||||
token_claims.insert("policy".to_string(), Value::String(policies.join(",")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally apply session policy
|
|
||||||
if let Some(policy_str) = session_policy {
|
|
||||||
populate_session_policy(&mut token_claims, policy_str)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate STS temp credentials
|
|
||||||
let secret = current_token_signing_key().ok_or_else(|| s3_error!(InternalError, "token signing key not initialized"))?;
|
|
||||||
|
|
||||||
let mut new_cred = get_new_credentials_with_metadata(&token_claims, &secret)
|
|
||||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("credential generation failed: {e}")))?;
|
|
||||||
|
|
||||||
new_cred.parent_user = parent_user;
|
|
||||||
new_cred.groups = Some(groups.to_vec());
|
|
||||||
|
|
||||||
// Store temp user in IAM
|
|
||||||
let iam_store =
|
|
||||||
crate::admin::runtime_sources::current_ready_iam_handle().map_err(|_| s3_error!(InternalError, "IAM not initialized"))?;
|
|
||||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
|
||||||
log_oidc_policy_diagnostics(&iam_store, provider_id, &new_cred.parent_user, policies, groups).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let updated_at = iam_store
|
|
||||||
.set_temp_user(&new_cred.access_key, &new_cred, None)
|
|
||||||
.await
|
|
||||||
.map_err(|_| s3_error!(InternalError, "failed to store temp user"))?;
|
|
||||||
|
|
||||||
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
|
|
||||||
r#type: "sts-credential".to_string(),
|
|
||||||
sts_credential: Some(SRSTSCredential {
|
|
||||||
access_key: new_cred.access_key.clone(),
|
|
||||||
secret_key: new_cred.secret_key.clone(),
|
|
||||||
session_token: new_cred.session_token.clone(),
|
|
||||||
parent_user: new_cred.parent_user.clone(),
|
|
||||||
parent_policy_mapping: policies.join(","),
|
|
||||||
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
|
||||||
}),
|
|
||||||
updated_at: Some(updated_at),
|
|
||||||
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("site replication OIDC STS hook failed, err: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(new_cred)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn populate_session_policy(claims: &mut HashMap<String, Value>, policy: &str) -> S3Result<()> {
|
|
||||||
if !policy.is_empty() {
|
|
||||||
let session_policy = Policy::parse_config(policy.as_bytes())
|
|
||||||
.map_err(|e| {
|
|
||||||
let error_msg = format!("Failed to parse session policy: {}. Please check that the policy is valid JSON format with standard brackets [] for arrays.", e);
|
|
||||||
S3Error::with_message(S3ErrorCode::InvalidRequest, error_msg)
|
|
||||||
})?;
|
|
||||||
if session_policy.version.is_empty() {
|
|
||||||
return Err(s3_error!(InvalidRequest, "invalid policy"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let policy_buf = serde_json::to_vec(&session_policy)
|
|
||||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal policy err {e}")))?;
|
|
||||||
|
|
||||||
if policy_buf.len() > 2048 {
|
|
||||||
return Err(s3_error!(InvalidRequest, "policy too large"));
|
|
||||||
}
|
|
||||||
|
|
||||||
claims.insert(
|
|
||||||
SESSION_POLICY_NAME.to_string(),
|
|
||||||
Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&policy_buf)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Escape special XML characters in a string.
|
/// Escape special XML characters in a string.
|
||||||
fn xml_escape(s: &str) -> String {
|
fn xml_escape(s: &str) -> String {
|
||||||
// Fast path: if there are no escapable characters, just clone the string.
|
// Fast path: if there are no escapable characters, just clone the string.
|
||||||
@@ -684,93 +423,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_has_identity_authorization_context() {
|
fn web_identity_errors_preserve_existing_s3_semantics() {
|
||||||
let empty: Vec<String> = vec![];
|
let cases = [
|
||||||
let groups = vec!["RustFS.ConsoleAdmin".to_string()];
|
(
|
||||||
let policies = vec!["consoleAdmin".to_string()];
|
FederationError::TokenVerification("invalid token".to_string()),
|
||||||
|
S3ErrorCode::AccessDenied,
|
||||||
|
"token verification failed: invalid token",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FederationError::NoAuthorizationContext,
|
||||||
|
S3ErrorCode::InvalidArgument,
|
||||||
|
"no policies are available for this OIDC token",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::InvalidRequest("invalid policy".to_string())),
|
||||||
|
S3ErrorCode::InvalidRequest,
|
||||||
|
"invalid policy",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FederationError::Binding(FederatedSessionBindingError::Internal("failed to store temp user".to_string())),
|
||||||
|
S3ErrorCode::InternalError,
|
||||||
|
"failed to store temp user",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
assert!(!has_identity_authorization_context(&empty, &empty));
|
for (error, expected_code, expected_message) in cases {
|
||||||
assert!(has_identity_authorization_context(&policies, &empty));
|
let error = web_identity_federation_error(error);
|
||||||
assert!(has_identity_authorization_context(&empty, &groups));
|
assert_eq!(error.code(), &expected_code);
|
||||||
}
|
assert_eq!(error.message(), Some(expected_message));
|
||||||
|
}
|
||||||
#[test]
|
|
||||||
fn test_extract_string_list_claim_supports_array_and_csv() {
|
|
||||||
let mut claims = HashMap::new();
|
|
||||||
claims.insert("roles".to_string(), serde_json::json!(["admin", "reader"]));
|
|
||||||
claims.insert("groups".to_string(), serde_json::json!("devs, ops"));
|
|
||||||
|
|
||||||
assert_eq!(extract_string_list_claim(&claims, "roles"), vec!["admin", "reader"]);
|
|
||||||
assert_eq!(extract_string_list_claim(&claims, "groups"), vec!["devs", "ops"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_string_list_claim_prefers_exact_match() {
|
|
||||||
let mut claims = HashMap::new();
|
|
||||||
claims.insert("Roles".to_string(), serde_json::json!(["mixed-case"]));
|
|
||||||
claims.insert("roles".to_string(), serde_json::json!(["exact-match"]));
|
|
||||||
|
|
||||||
assert_eq!(extract_string_list_claim(&claims, "roles"), vec!["exact-match"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_string_list_claim_ambiguous_case_insensitive_match_returns_empty() {
|
|
||||||
let mut claims = HashMap::new();
|
|
||||||
claims.insert("Roles".to_string(), serde_json::json!(["mixed-case"]));
|
|
||||||
claims.insert("ROLES".to_string(), serde_json::json!(["upper-case"]));
|
|
||||||
|
|
||||||
assert!(extract_string_list_claim(&claims, "roles").is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_build_oidc_token_claims_includes_normalized_roles() {
|
|
||||||
let mut raw = HashMap::new();
|
|
||||||
raw.insert("Roles".to_string(), serde_json::json!("admin, reader"));
|
|
||||||
let claims = OidcClaims {
|
|
||||||
sub: "user-sub".to_string(),
|
|
||||||
raw,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let token_claims = build_oidc_token_claims(&claims, "default", &["devs".to_string()], Some("roles"));
|
|
||||||
|
|
||||||
assert_eq!(token_claims.get("roles"), Some(&serde_json::json!(["admin", "reader"])));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_configured_roles_claim_key_requires_explicit_config() {
|
|
||||||
assert_eq!(configured_roles_claim_key("default"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_resolve_oidc_session_identity_prefers_username_over_email() {
|
|
||||||
let claims = OidcClaims {
|
|
||||||
username: "john".to_string(),
|
|
||||||
email: "john@example.com".to_string(),
|
|
||||||
sub: "sub-1".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(resolve_oidc_session_identity(&claims), "john");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_resolve_oidc_session_identity_falls_back_to_email_then_sub() {
|
|
||||||
let claims_with_email = OidcClaims {
|
|
||||||
email: "john@example.com".to_string(),
|
|
||||||
sub: "sub-1".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(resolve_oidc_session_identity(&claims_with_email), "john@example.com");
|
|
||||||
|
|
||||||
let claims_with_sub = OidcClaims {
|
|
||||||
sub: "sub-1".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(resolve_oidc_session_identity(&claims_with_sub), "sub-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_resolve_oidc_session_identity_uses_unknown_when_all_empty() {
|
|
||||||
assert_eq!(resolve_oidc_session_identity(&OidcClaims::default()), "oidc-user-unknown");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ use crate::app::object_usecase::DefaultObjectUsecase;
|
|||||||
use crate::runtime_sources as root_runtime_sources;
|
use crate::runtime_sources as root_runtime_sources;
|
||||||
pub(crate) use crate::runtime_sources::{
|
pub(crate) use crate::runtime_sources::{
|
||||||
AppContext, ServerContextSlot, current_action_credentials, current_boot_time, current_bucket_metadata_handle,
|
AppContext, ServerContextSlot, current_action_credentials, current_boot_time, current_bucket_metadata_handle,
|
||||||
current_bucket_monitor_handle, current_deployment_id, current_endpoints_handle, current_iam_handle,
|
current_bucket_monitor_handle, current_deployment_id, current_endpoints_handle, current_federated_identity_service,
|
||||||
current_kms_runtime_service_manager, current_notification_system_for_context, current_object_data_cache_handle_for_context,
|
current_iam_handle, current_kms_runtime_service_manager, current_notification_system_for_context,
|
||||||
current_object_store_handle_for_context, current_oidc_handle, current_ready_iam_handle, current_region,
|
current_object_data_cache_handle_for_context, current_object_store_handle_for_context, current_ready_iam_handle,
|
||||||
current_replication_pool_handle, current_replication_stats_handle, current_server_config_for_context,
|
current_region, current_replication_pool_handle, current_replication_stats_handle, current_server_config_for_context,
|
||||||
current_token_signing_key,
|
current_token_signing_key,
|
||||||
};
|
};
|
||||||
use rustfs_config::server_config::Config;
|
use rustfs_config::server_config::Config;
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
// 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::session_policy::populate_session_policy;
|
||||||
|
use crate::admin::{
|
||||||
|
handlers::site_replication::site_replication_iam_change_hook,
|
||||||
|
runtime_sources::{current_ready_iam_handle, current_token_signing_key},
|
||||||
|
};
|
||||||
|
use rustfs_iam::{
|
||||||
|
federation::{FederatedSessionBinding, FederatedSessionBindingError, FederatedSessionTransaction},
|
||||||
|
store::object::ObjectStore,
|
||||||
|
sys::IamSys,
|
||||||
|
};
|
||||||
|
use rustfs_madmin::{SITE_REPL_API_VERSION, SRIAMItem, SRSTSCredential};
|
||||||
|
use rustfs_policy::auth::get_new_credentials_with_metadata;
|
||||||
|
use s3s::{S3Error, S3ErrorCode};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
pub(crate) struct DefaultFederatedSessionBinding;
|
||||||
|
|
||||||
|
fn build_oidc_token_claims(transaction: &FederatedSessionTransaction) -> HashMap<String, Value> {
|
||||||
|
let authorization = &transaction.authorization;
|
||||||
|
let claims = &authorization.claims;
|
||||||
|
let mut token_claims = HashMap::new();
|
||||||
|
token_claims.insert("sub".to_string(), Value::String(claims.sub.clone()));
|
||||||
|
token_claims.insert("iss".to_string(), Value::String("rustfs-oidc".to_string()));
|
||||||
|
token_claims.insert("oidc_provider".to_string(), Value::String(authorization.provider_id.clone()));
|
||||||
|
|
||||||
|
if !claims.email.is_empty() {
|
||||||
|
token_claims.insert("email".to_string(), Value::String(claims.email.clone()));
|
||||||
|
}
|
||||||
|
if !claims.username.is_empty() {
|
||||||
|
token_claims.insert("preferred_username".to_string(), Value::String(claims.username.clone()));
|
||||||
|
}
|
||||||
|
if !authorization.groups.is_empty() {
|
||||||
|
token_claims.insert(
|
||||||
|
"groups".to_string(),
|
||||||
|
Value::Array(
|
||||||
|
authorization
|
||||||
|
.groups
|
||||||
|
.iter()
|
||||||
|
.map(|group| Value::String(group.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !authorization.roles.is_empty() {
|
||||||
|
token_claims.insert(
|
||||||
|
"roles".to_string(),
|
||||||
|
Value::Array(authorization.roles.iter().cloned().map(Value::String).collect()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
token_claims
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn log_oidc_policy_diagnostics(
|
||||||
|
iam_store: &IamSys<ObjectStore>,
|
||||||
|
provider_id: &str,
|
||||||
|
parent_user: &str,
|
||||||
|
policies: &[String],
|
||||||
|
groups: &[String],
|
||||||
|
) {
|
||||||
|
if policies.is_empty() {
|
||||||
|
let policy_documents = BTreeMap::<String, Value>::new();
|
||||||
|
let missing_policies = Vec::<String>::new();
|
||||||
|
let combined_policy = Value::Null;
|
||||||
|
debug!(
|
||||||
|
provider_id = %provider_id,
|
||||||
|
parent_user = %parent_user,
|
||||||
|
policy_count = 0,
|
||||||
|
group_count = groups.len(),
|
||||||
|
policies = ?policies,
|
||||||
|
groups = ?groups,
|
||||||
|
policy_documents = ?policy_documents,
|
||||||
|
missing_policies = ?missing_policies,
|
||||||
|
combined_policy = ?combined_policy,
|
||||||
|
"OIDC STS policy diagnostics"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match iam_store.list_policy_docs("").await {
|
||||||
|
Ok(policy_docs) => {
|
||||||
|
let mut policy_documents = BTreeMap::new();
|
||||||
|
let mut missing_policies = Vec::new();
|
||||||
|
for policy_name in policies {
|
||||||
|
match policy_docs.get(policy_name) {
|
||||||
|
Some(policy_doc) => {
|
||||||
|
let policy_doc_json = serde_json::to_value(policy_doc).unwrap_or_else(|err| {
|
||||||
|
serde_json::json!({
|
||||||
|
"serialization_error": err.to_string(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
policy_documents.insert(policy_name.clone(), policy_doc_json);
|
||||||
|
}
|
||||||
|
None => missing_policies.push(policy_name.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let combined_policy = iam_store.get_combined_policy(policies).await;
|
||||||
|
let combined_policy_json = serde_json::to_value(&combined_policy).unwrap_or_else(|err| {
|
||||||
|
serde_json::json!({
|
||||||
|
"serialization_error": err.to_string(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
provider_id = %provider_id,
|
||||||
|
parent_user = %parent_user,
|
||||||
|
policy_count = policies.len(),
|
||||||
|
group_count = groups.len(),
|
||||||
|
policies = ?policies,
|
||||||
|
groups = ?groups,
|
||||||
|
policy_documents = ?policy_documents,
|
||||||
|
missing_policies = ?missing_policies,
|
||||||
|
combined_policy = ?combined_policy_json,
|
||||||
|
"OIDC STS policy diagnostics"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
provider_id = %provider_id,
|
||||||
|
parent_user = %parent_user,
|
||||||
|
policy_count = policies.len(),
|
||||||
|
group_count = groups.len(),
|
||||||
|
policies = ?policies,
|
||||||
|
groups = ?groups,
|
||||||
|
error = %err,
|
||||||
|
"OIDC STS policy diagnostics failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binding_error_from_s3(error: S3Error) -> FederatedSessionBindingError {
|
||||||
|
let message = error.message().unwrap_or("federated session binding failed").to_string();
|
||||||
|
if error.code() == &S3ErrorCode::InvalidRequest {
|
||||||
|
FederatedSessionBindingError::InvalidRequest(message)
|
||||||
|
} else {
|
||||||
|
FederatedSessionBindingError::Internal(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn issue_credentials(
|
||||||
|
transaction: &FederatedSessionTransaction,
|
||||||
|
secret: Option<&str>,
|
||||||
|
) -> Result<rustfs_credentials::Credentials, FederatedSessionBindingError> {
|
||||||
|
let authorization = &transaction.authorization;
|
||||||
|
let claims = &authorization.claims;
|
||||||
|
let mut token_claims = build_oidc_token_claims(transaction);
|
||||||
|
let duration = i64::try_from(transaction.duration_seconds)
|
||||||
|
.map_err(|_| FederatedSessionBindingError::InvalidRequest("invalid duration".to_string()))?;
|
||||||
|
let exp = OffsetDateTime::now_utc().saturating_add(Duration::seconds(duration));
|
||||||
|
token_claims.insert("exp".to_string(), Value::Number(serde_json::Number::from(exp.unix_timestamp())));
|
||||||
|
|
||||||
|
let parent_user = claims.session_identity();
|
||||||
|
debug!(
|
||||||
|
provider_id = %authorization.provider_id,
|
||||||
|
parent_user = %parent_user,
|
||||||
|
email = %claims.email,
|
||||||
|
username = %claims.username,
|
||||||
|
sub = %claims.sub,
|
||||||
|
policy_count = authorization.policies.len(),
|
||||||
|
group_count = authorization.groups.len(),
|
||||||
|
policies = ?authorization.policies,
|
||||||
|
groups = ?authorization.groups,
|
||||||
|
roles_claim_key = ?authorization.roles_claim_key,
|
||||||
|
has_session_policy = transaction.session_policy.is_some(),
|
||||||
|
"OIDC STS credential claims prepared"
|
||||||
|
);
|
||||||
|
token_claims.insert("parent".to_string(), Value::String(parent_user.clone()));
|
||||||
|
|
||||||
|
if !authorization.policies.is_empty() {
|
||||||
|
token_claims.insert("policy".to_string(), Value::String(authorization.policies.join(",")));
|
||||||
|
}
|
||||||
|
if let Some(policy) = transaction.session_policy.as_deref() {
|
||||||
|
populate_session_policy(&mut token_claims, policy).map_err(binding_error_from_s3)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret = secret.ok_or_else(|| FederatedSessionBindingError::Internal("token signing key not initialized".to_string()))?;
|
||||||
|
let mut credentials = get_new_credentials_with_metadata(&token_claims, secret)
|
||||||
|
.map_err(|error| FederatedSessionBindingError::Internal(format!("credential generation failed: {error}")))?;
|
||||||
|
credentials.parent_user = parent_user;
|
||||||
|
credentials.groups = Some(authorization.groups.clone());
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn site_replication_item(
|
||||||
|
credentials: &rustfs_credentials::Credentials,
|
||||||
|
transaction: &FederatedSessionTransaction,
|
||||||
|
updated_at: OffsetDateTime,
|
||||||
|
) -> SRIAMItem {
|
||||||
|
SRIAMItem {
|
||||||
|
r#type: "sts-credential".to_string(),
|
||||||
|
sts_credential: Some(SRSTSCredential {
|
||||||
|
access_key: credentials.access_key.clone(),
|
||||||
|
secret_key: credentials.secret_key.clone(),
|
||||||
|
session_token: credentials.session_token.clone(),
|
||||||
|
parent_user: credentials.parent_user.clone(),
|
||||||
|
parent_policy_mapping: transaction.authorization.policies.join(","),
|
||||||
|
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
||||||
|
}),
|
||||||
|
updated_at: Some(updated_at),
|
||||||
|
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FederatedSessionBinding for DefaultFederatedSessionBinding {
|
||||||
|
async fn bind(
|
||||||
|
&self,
|
||||||
|
transaction: &FederatedSessionTransaction,
|
||||||
|
) -> Result<rustfs_credentials::Credentials, FederatedSessionBindingError> {
|
||||||
|
let authorization = &transaction.authorization;
|
||||||
|
let secret = current_token_signing_key();
|
||||||
|
let credentials = issue_credentials(transaction, secret.as_deref())?;
|
||||||
|
|
||||||
|
let iam_store =
|
||||||
|
current_ready_iam_handle().map_err(|_| FederatedSessionBindingError::Internal("IAM not initialized".to_string()))?;
|
||||||
|
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||||
|
log_oidc_policy_diagnostics(
|
||||||
|
&iam_store,
|
||||||
|
&authorization.provider_id,
|
||||||
|
&credentials.parent_user,
|
||||||
|
&authorization.policies,
|
||||||
|
&authorization.groups,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated_at = iam_store
|
||||||
|
.set_temp_user(&credentials.access_key, &credentials, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| FederatedSessionBindingError::Internal("failed to store temp user".to_string()))?;
|
||||||
|
|
||||||
|
if let Err(err) = site_replication_iam_change_hook(site_replication_item(&credentials, transaction, updated_at)).await {
|
||||||
|
warn!("site replication OIDC STS hook failed, err: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rustfs_iam::federation::{FederatedAuthorization, FederatedClaims};
|
||||||
|
|
||||||
|
fn transaction() -> FederatedSessionTransaction {
|
||||||
|
FederatedSessionTransaction {
|
||||||
|
authorization: FederatedAuthorization {
|
||||||
|
provider_id: "default".to_string(),
|
||||||
|
claims: FederatedClaims {
|
||||||
|
sub: "subject".to_string(),
|
||||||
|
email: "user@example.com".to_string(),
|
||||||
|
username: "user".to_string(),
|
||||||
|
groups: vec!["source-group".to_string()],
|
||||||
|
raw: HashMap::new(),
|
||||||
|
},
|
||||||
|
policies: vec!["readwrite".to_string()],
|
||||||
|
groups: vec!["devs".to_string()],
|
||||||
|
roles_claim_key: Some("roles".to_string()),
|
||||||
|
roles: vec!["admin".to_string(), "reader".to_string()],
|
||||||
|
},
|
||||||
|
duration_seconds: 3600,
|
||||||
|
session_policy: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_claims_preserve_existing_oidc_shape() {
|
||||||
|
let transaction = transaction();
|
||||||
|
|
||||||
|
let claims = build_oidc_token_claims(&transaction);
|
||||||
|
assert_eq!(claims.get("sub"), Some(&serde_json::json!("subject")));
|
||||||
|
assert_eq!(claims.get("iss"), Some(&serde_json::json!("rustfs-oidc")));
|
||||||
|
assert_eq!(claims.get("oidc_provider"), Some(&serde_json::json!("default")));
|
||||||
|
assert_eq!(claims.get("email"), Some(&serde_json::json!("user@example.com")));
|
||||||
|
assert_eq!(claims.get("preferred_username"), Some(&serde_json::json!("user")));
|
||||||
|
assert_eq!(claims.get("groups"), Some(&serde_json::json!(["devs"])));
|
||||||
|
assert_eq!(claims.get("roles"), Some(&serde_json::json!(["admin", "reader"])));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn issued_credentials_and_replication_item_preserve_existing_shape() {
|
||||||
|
let transaction = transaction();
|
||||||
|
let secret = "federated-session-test-signing-secret";
|
||||||
|
|
||||||
|
let credentials = issue_credentials(&transaction, Some(secret)).expect("credential issuance should succeed");
|
||||||
|
assert_eq!(credentials.parent_user, "user");
|
||||||
|
assert_eq!(credentials.groups, Some(vec!["devs".to_string()]));
|
||||||
|
|
||||||
|
let claims = rustfs_iam::sys::get_claims_from_token_with_secret(&credentials.session_token, secret)
|
||||||
|
.expect("issued session token should verify");
|
||||||
|
assert_eq!(claims.get("iss"), Some(&serde_json::json!("rustfs-oidc")));
|
||||||
|
assert_eq!(claims.get("oidc_provider"), Some(&serde_json::json!("default")));
|
||||||
|
assert_eq!(claims.get("parent"), Some(&serde_json::json!("user")));
|
||||||
|
assert_eq!(claims.get("policy"), Some(&serde_json::json!("readwrite")));
|
||||||
|
assert_eq!(claims.get("groups"), Some(&serde_json::json!(["devs"])));
|
||||||
|
assert_eq!(claims.get("roles"), Some(&serde_json::json!(["admin", "reader"])));
|
||||||
|
assert!(!claims.contains_key("oidc_issuer"));
|
||||||
|
|
||||||
|
let updated_at = OffsetDateTime::UNIX_EPOCH;
|
||||||
|
let item = site_replication_item(&credentials, &transaction, updated_at);
|
||||||
|
assert_eq!(item.r#type, "sts-credential");
|
||||||
|
assert_eq!(item.updated_at, Some(updated_at));
|
||||||
|
assert_eq!(item.api_version.as_deref(), Some(SITE_REPL_API_VERSION));
|
||||||
|
let replicated = item.sts_credential.expect("replication item should contain STS credentials");
|
||||||
|
assert_eq!(replicated.access_key, credentials.access_key);
|
||||||
|
assert_eq!(replicated.secret_key, credentials.secret_key);
|
||||||
|
assert_eq!(replicated.session_token, credentials.session_token);
|
||||||
|
assert_eq!(replicated.parent_user, "user");
|
||||||
|
assert_eq!(replicated.parent_policy_mapping, "readwrite");
|
||||||
|
assert_eq!(replicated.api_version.as_deref(), Some(SITE_REPL_API_VERSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_session_policy_precedes_missing_signing_key() {
|
||||||
|
let mut transaction = transaction();
|
||||||
|
transaction.session_policy = Some("not-json".to_string());
|
||||||
|
|
||||||
|
let error = issue_credentials(&transaction, None).expect_err("invalid policy should fail first");
|
||||||
|
assert!(matches!(error, FederatedSessionBindingError::InvalidRequest(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,6 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub(crate) mod federated_identity;
|
||||||
|
pub(crate) mod session_policy;
|
||||||
pub mod site_replication;
|
pub mod site_replication;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// 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 rustfs_iam::sys::SESSION_POLICY_NAME;
|
||||||
|
use rustfs_policy::policy::Policy;
|
||||||
|
use s3s::{S3Error, S3ErrorCode, S3Result, s3_error};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub(crate) fn populate_session_policy(claims: &mut HashMap<String, Value>, policy: &str) -> S3Result<()> {
|
||||||
|
if !policy.is_empty() {
|
||||||
|
let session_policy = Policy::parse_config(policy.as_bytes()).map_err(|e| {
|
||||||
|
let error_msg = format!(
|
||||||
|
"Failed to parse session policy: {}. Please check that the policy is valid JSON format with standard brackets [] for arrays.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
S3Error::with_message(S3ErrorCode::InvalidRequest, error_msg)
|
||||||
|
})?;
|
||||||
|
if session_policy.version.is_empty() {
|
||||||
|
return Err(s3_error!(InvalidRequest, "invalid policy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let policy_buf = serde_json::to_vec(&session_policy)
|
||||||
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal policy err {e}")))?;
|
||||||
|
|
||||||
|
if policy_buf.len() > 2048 {
|
||||||
|
return Err(s3_error!(InvalidRequest, "policy too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.insert(
|
||||||
|
SESSION_POLICY_NAME.to_string(),
|
||||||
|
Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&policy_buf)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_policy_does_not_change_claims() {
|
||||||
|
let mut claims = HashMap::new();
|
||||||
|
populate_session_policy(&mut claims, "").expect("empty policy should be accepted");
|
||||||
|
assert!(claims.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_policy_is_encoded_in_the_session_claim() {
|
||||||
|
let mut claims = HashMap::new();
|
||||||
|
let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"]}]}"#;
|
||||||
|
|
||||||
|
populate_session_policy(&mut claims, policy).expect("valid policy should be accepted");
|
||||||
|
|
||||||
|
let encoded = claims
|
||||||
|
.get(SESSION_POLICY_NAME)
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.expect("session policy claim should be present");
|
||||||
|
let decoded = base64_simd::URL_SAFE_NO_PAD
|
||||||
|
.decode_to_vec(encoded.as_bytes())
|
||||||
|
.expect("session policy claim should be base64url encoded");
|
||||||
|
let decoded_policy: Value = serde_json::from_slice(&decoded).expect("session policy claim should contain JSON");
|
||||||
|
assert_eq!(decoded_policy["Version"], "2012-10-17");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_policy_is_rejected_as_invalid_request() {
|
||||||
|
let mut claims = HashMap::new();
|
||||||
|
let error = populate_session_policy(&mut claims, "not-json").expect_err("invalid policy should fail");
|
||||||
|
assert_eq!(error.code(), &S3ErrorCode::InvalidRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-35
@@ -38,7 +38,7 @@ use crate::app::object_data_cache::ObjectDataCacheAdapter;
|
|||||||
use crate::config::RustFSBufferConfig;
|
use crate::config::RustFSBufferConfig;
|
||||||
use rustfs_config::server_config::Config;
|
use rustfs_config::server_config::Config;
|
||||||
use rustfs_credentials::Credentials;
|
use rustfs_credentials::Credentials;
|
||||||
use rustfs_iam::{error::Error as IamError, oidc::OidcSys, store::object::ObjectStore, sys::IamSys};
|
use rustfs_iam::{error::Error as IamError, federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
|
||||||
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
||||||
use rustfs_kms::{KmsServiceManager, ObjectEncryptionService};
|
use rustfs_kms::{KmsServiceManager, ObjectEncryptionService};
|
||||||
use rustfs_lock::LockClient;
|
use rustfs_lock::LockClient;
|
||||||
@@ -88,14 +88,14 @@ pub fn resolve_iam_handle() -> Option<Arc<IamSys<ObjectStore>>> {
|
|||||||
resolve_iam_handle_with(get_global_app_context())
|
resolve_iam_handle_with(get_global_app_context())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve OIDC system handle using AppContext-first precedence.
|
/// Resolve the federated identity service using AppContext-first precedence.
|
||||||
pub fn resolve_oidc_handle() -> Option<Arc<OidcSys>> {
|
pub fn resolve_federated_identity_service() -> Option<Arc<FederatedIdentityService>> {
|
||||||
resolve_oidc_handle_with(get_global_app_context())
|
resolve_federated_identity_service_with(get_global_app_context())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish the initialized OIDC system handle into the global AppContext.
|
/// Publish the initialized federated identity service into the global AppContext.
|
||||||
pub fn publish_oidc_handle(oidc: Arc<OidcSys>) -> bool {
|
pub fn publish_federated_identity_service(service: Arc<FederatedIdentityService>) -> bool {
|
||||||
publish_oidc_handle_with(get_global_app_context(), oidc)
|
handles::publish_default_federated_identity_service(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a ready IAM system handle using AppContext-first precedence.
|
/// Resolve a ready IAM system handle using AppContext-first precedence.
|
||||||
@@ -329,12 +329,13 @@ fn resolve_iam_handle_with(context: Option<Arc<AppContext>>) -> Option<Arc<IamSy
|
|||||||
context.map(|context| context.iam().handle())
|
context.map(|context| context.iam().handle())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_oidc_handle_with(context: Option<Arc<AppContext>>) -> Option<Arc<OidcSys>> {
|
fn resolve_federated_identity_service_with(context: Option<Arc<AppContext>>) -> Option<Arc<FederatedIdentityService>> {
|
||||||
context.and_then(|context| context.oidc().handle())
|
context.and_then(|context| context.federated_identity().handle())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publish_oidc_handle_with(context: Option<Arc<AppContext>>, oidc: Arc<OidcSys>) -> bool {
|
#[cfg(test)]
|
||||||
context.is_some_and(|context| context.publish_oidc_handle(oidc))
|
fn publish_federated_identity_service_with(context: Option<Arc<AppContext>>, service: Arc<FederatedIdentityService>) -> bool {
|
||||||
|
context.is_some_and(|context| context.publish_federated_identity_service(service))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_ready_iam_handle_with(context: Arc<AppContext>) -> rustfs_iam::error::Result<Arc<IamSys<ObjectStore>>> {
|
fn resolve_ready_iam_handle_with(context: Arc<AppContext>) -> rustfs_iam::error::Result<Arc<IamSys<ObjectStore>>> {
|
||||||
@@ -509,14 +510,19 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use crate::app::context::interfaces::{
|
use crate::app::context::interfaces::{
|
||||||
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BufferConfigInterface, DeploymentIdInterface,
|
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BufferConfigInterface, DeploymentIdInterface,
|
||||||
EndpointsInterface, IamInterface, InternodeMetricsInterface, KmsInterface, KmsRuntimeInterface, LocalNodeNameInterface,
|
EndpointsInterface, FederatedIdentityInterface, IamInterface, InternodeMetricsInterface, KmsInterface,
|
||||||
LockClientInterface, LockClientsInterface, OidcInterface, OutboundTlsRuntimeInterface, PerformanceMetricsInterface,
|
KmsRuntimeInterface, LocalNodeNameInterface, LockClientInterface, LockClientsInterface, OutboundTlsRuntimeInterface,
|
||||||
RegionInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface, ScannerMetricsInterface,
|
PerformanceMetricsInterface, RegionInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface,
|
||||||
ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
|
ScannerMetricsInterface, ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
|
||||||
};
|
};
|
||||||
use crate::config::{RustFSBufferConfig, WorkloadProfile};
|
use crate::config::{RustFSBufferConfig, WorkloadProfile};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustfs_iam::{oidc::OidcSys, store::object::ObjectStore, sys::IamSys};
|
use rustfs_iam::{
|
||||||
|
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
|
||||||
|
oidc::OidcSys,
|
||||||
|
store::object::ObjectStore,
|
||||||
|
sys::IamSys,
|
||||||
|
};
|
||||||
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
||||||
use rustfs_lock::{LocalClient, LockClient};
|
use rustfs_lock::{LocalClient, LockClient};
|
||||||
use rustfs_s3select_api::{
|
use rustfs_s3select_api::{
|
||||||
@@ -552,32 +558,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestOidcInterface {
|
struct TestFederatedIdentityInterface {
|
||||||
oidc: StdRwLock<Option<Arc<OidcSys>>>,
|
service: StdRwLock<Option<Arc<FederatedIdentityService>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestOidcInterface {
|
impl TestFederatedIdentityInterface {
|
||||||
fn new(oidc: Option<Arc<OidcSys>>) -> Self {
|
fn new(service: Option<Arc<FederatedIdentityService>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
oidc: StdRwLock::new(oidc),
|
service: StdRwLock::new(service),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OidcInterface for TestOidcInterface {
|
impl FederatedIdentityInterface for TestFederatedIdentityInterface {
|
||||||
fn handle(&self) -> Option<Arc<rustfs_iam::oidc::OidcSys>> {
|
fn handle(&self) -> Option<Arc<FederatedIdentityService>> {
|
||||||
self.oidc.read().ok().and_then(|oidc| oidc.as_ref().cloned())
|
self.service.read().ok().and_then(|service| service.as_ref().cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publish_handle(&self, oidc: Arc<rustfs_iam::oidc::OidcSys>) -> bool {
|
fn publish_handle(&self, service: Arc<FederatedIdentityService>) -> bool {
|
||||||
let Ok(mut published_oidc) = self.oidc.write() else {
|
let Ok(mut published_service) = self.service.write() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
*published_oidc = Some(oidc);
|
*published_service = Some(service);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_federated_identity_service(oidc: OidcSys) -> Arc<FederatedIdentityService> {
|
||||||
|
let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc)));
|
||||||
|
Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter)))
|
||||||
|
}
|
||||||
|
|
||||||
struct TestKmsInterface {
|
struct TestKmsInterface {
|
||||||
kms: Arc<KmsServiceManager>,
|
kms: Arc<KmsServiceManager>,
|
||||||
}
|
}
|
||||||
@@ -1001,8 +1012,8 @@ mod tests {
|
|||||||
Ok(sys) => sys,
|
Ok(sys) => sys,
|
||||||
Err(err) => unreachable!("test OIDC fallback sys should initialize: {err}"),
|
Err(err) => unreachable!("test OIDC fallback sys should initialize: {err}"),
|
||||||
};
|
};
|
||||||
let context_oidc = Arc::new(context_oidc_sys);
|
let context_oidc = test_federated_identity_service(context_oidc_sys);
|
||||||
let fallback_oidc = Arc::new(fallback_oidc_sys);
|
let fallback_oidc = test_federated_identity_service(fallback_oidc_sys);
|
||||||
let context_token_signing_key = "context-token-signing-key".to_string();
|
let context_token_signing_key = "context-token-signing-key".to_string();
|
||||||
|
|
||||||
let context = Arc::new(AppContext::with_test_interfaces(
|
let context = Arc::new(AppContext::with_test_interfaces(
|
||||||
@@ -1012,7 +1023,7 @@ mod tests {
|
|||||||
ready: true,
|
ready: true,
|
||||||
token_signing_key: Some(context_token_signing_key.clone()),
|
token_signing_key: Some(context_token_signing_key.clone()),
|
||||||
}),
|
}),
|
||||||
oidc: Arc::new(TestOidcInterface::new(Some(context_oidc.clone()))),
|
federated_identity: Arc::new(TestFederatedIdentityInterface::new(None)),
|
||||||
kms: Arc::new(TestKmsInterface {
|
kms: Arc::new(TestKmsInterface {
|
||||||
kms: context_kms.clone(),
|
kms: context_kms.clone(),
|
||||||
}),
|
}),
|
||||||
@@ -1112,10 +1123,12 @@ mod tests {
|
|||||||
context_outbound_tls_state.generation
|
context_outbound_tls_state.generation
|
||||||
);
|
);
|
||||||
assert!(resolve_iam_ready_with(Some(context.clone())).expect("context IAM ready"));
|
assert!(resolve_iam_ready_with(Some(context.clone())).expect("context IAM ready"));
|
||||||
let resolved_oidc = resolve_oidc_handle_with(Some(context.clone()));
|
assert!(resolve_federated_identity_service_with(Some(context.clone())).is_none());
|
||||||
|
assert!(publish_federated_identity_service_with(Some(context.clone()), context_oidc.clone()));
|
||||||
|
let resolved_oidc = resolve_federated_identity_service_with(Some(context.clone()));
|
||||||
assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &context_oidc)));
|
assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &context_oidc)));
|
||||||
assert!(publish_oidc_handle_with(Some(context.clone()), fallback_oidc.clone()));
|
assert!(publish_federated_identity_service_with(Some(context.clone()), fallback_oidc.clone()));
|
||||||
let resolved_oidc = resolve_oidc_handle_with(Some(context.clone()));
|
let resolved_oidc = resolve_federated_identity_service_with(Some(context.clone()));
|
||||||
assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &fallback_oidc)));
|
assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &fallback_oidc)));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_token_signing_key_with(Some(context.clone())).as_deref(),
|
resolve_token_signing_key_with(Some(context.clone())).as_deref(),
|
||||||
@@ -1236,10 +1249,10 @@ mod tests {
|
|||||||
assert!(resolve_outbound_tls_state_with(None).await.is_none());
|
assert!(resolve_outbound_tls_state_with(None).await.is_none());
|
||||||
assert!(resolve_iam_ready_with(None).is_none());
|
assert!(resolve_iam_ready_with(None).is_none());
|
||||||
assert!(resolve_iam_handle_with(None).is_none());
|
assert!(resolve_iam_handle_with(None).is_none());
|
||||||
assert!(resolve_oidc_handle_with(None).is_none());
|
assert!(resolve_federated_identity_service_with(None).is_none());
|
||||||
assert!(resolve_token_signing_key_with(None).is_none());
|
assert!(resolve_token_signing_key_with(None).is_none());
|
||||||
assert!(resolve_notify_interface_for_context(None).is_none());
|
assert!(resolve_notify_interface_for_context(None).is_none());
|
||||||
assert!(!publish_oidc_handle_with(None, context_oidc));
|
assert!(!publish_federated_identity_service_with(None, context_oidc));
|
||||||
assert!(resolve_bucket_metadata_handle_with(None).is_none());
|
assert!(resolve_bucket_metadata_handle_with(None).is_none());
|
||||||
assert!(resolve_bucket_monitor_handle_with(None).is_none());
|
assert!(resolve_bucket_monitor_handle_with(None).is_none());
|
||||||
assert!(resolve_replication_pool_handle_with(None).is_none());
|
assert!(resolve_replication_pool_handle_with(None).is_none());
|
||||||
|
|||||||
@@ -17,24 +17,25 @@ use super::super::storage_api::context::runtime::set_object_store_resolver;
|
|||||||
use super::handles::{
|
use super::handles::{
|
||||||
IamHandle, KmsHandle, default_action_credential_interface, default_boot_time_interface, default_bucket_metadata_interface,
|
IamHandle, KmsHandle, default_action_credential_interface, default_boot_time_interface, default_bucket_metadata_interface,
|
||||||
default_bucket_monitor_interface, default_buffer_config_interface, default_deployment_id_interface,
|
default_bucket_monitor_interface, default_buffer_config_interface, default_deployment_id_interface,
|
||||||
default_endpoints_interface, default_expiry_state_interface, default_internode_metrics_interface,
|
default_endpoints_interface, default_expiry_state_interface, default_federated_identity_interface,
|
||||||
default_kms_runtime_interface, default_local_node_name_interface, default_lock_client_interface,
|
default_internode_metrics_interface, default_kms_runtime_interface, default_local_node_name_interface,
|
||||||
default_lock_clients_interface, default_notification_system_interface, default_notify_interface, default_oidc_interface,
|
default_lock_client_interface, default_lock_clients_interface, default_notification_system_interface,
|
||||||
default_outbound_tls_runtime_interface, default_performance_metrics_interface, default_region_interface,
|
default_notify_interface, default_outbound_tls_runtime_interface, default_performance_metrics_interface,
|
||||||
default_replication_pool_interface, default_replication_stats_interface, default_runtime_port_interface,
|
default_region_interface, default_replication_pool_interface, default_replication_stats_interface,
|
||||||
default_s3select_db_interface, default_scanner_metrics_interface, default_server_config_interface,
|
default_runtime_port_interface, default_s3select_db_interface, default_scanner_metrics_interface,
|
||||||
default_storage_class_interface, default_tier_config_interface, default_transition_state_interface, oidc_interface,
|
default_server_config_interface, default_storage_class_interface, default_tier_config_interface,
|
||||||
|
default_transition_state_interface,
|
||||||
};
|
};
|
||||||
use super::interfaces::{
|
use super::interfaces::{
|
||||||
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BucketMonitorInterface, BufferConfigInterface,
|
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BucketMonitorInterface, BufferConfigInterface,
|
||||||
DeploymentIdInterface, EndpointsInterface, ExpiryStateInterface, IamInterface, InternodeMetricsInterface, KmsInterface,
|
DeploymentIdInterface, EndpointsInterface, ExpiryStateInterface, FederatedIdentityInterface, IamInterface,
|
||||||
KmsRuntimeInterface, LocalNodeNameInterface, LockClientInterface, LockClientsInterface, NotificationSystemInterface,
|
InternodeMetricsInterface, KmsInterface, KmsRuntimeInterface, LocalNodeNameInterface, LockClientInterface,
|
||||||
NotifyInterface, OidcInterface, OutboundTlsRuntimeInterface, PerformanceMetricsInterface, RegionInterface,
|
LockClientsInterface, NotificationSystemInterface, NotifyInterface, OutboundTlsRuntimeInterface, PerformanceMetricsInterface,
|
||||||
ReplicationPoolInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface, ScannerMetricsInterface,
|
RegionInterface, ReplicationPoolInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface,
|
||||||
ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
|
ScannerMetricsInterface, ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
|
||||||
};
|
};
|
||||||
use crate::app::object_data_cache::ObjectDataCacheAdapter;
|
use crate::app::object_data_cache::ObjectDataCacheAdapter;
|
||||||
use rustfs_iam::{oidc::OidcSys, store::object::ObjectStore, sys::IamSys};
|
use rustfs_iam::{federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
|
||||||
use rustfs_kms::KmsServiceManager;
|
use rustfs_kms::KmsServiceManager;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ use std::sync::{Arc, OnceLock};
|
|||||||
pub struct AppContext {
|
pub struct AppContext {
|
||||||
object_store: Arc<ECStore>,
|
object_store: Arc<ECStore>,
|
||||||
iam: Arc<dyn IamInterface>,
|
iam: Arc<dyn IamInterface>,
|
||||||
oidc: Arc<dyn OidcInterface>,
|
federated_identity: Arc<dyn FederatedIdentityInterface>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
kms: Arc<dyn KmsInterface>,
|
kms: Arc<dyn KmsInterface>,
|
||||||
kms_runtime: Arc<dyn KmsRuntimeInterface>,
|
kms_runtime: Arc<dyn KmsRuntimeInterface>,
|
||||||
@@ -91,7 +92,7 @@ impl AppContext {
|
|||||||
Self {
|
Self {
|
||||||
object_store,
|
object_store,
|
||||||
iam,
|
iam,
|
||||||
oidc: default_oidc_interface(),
|
federated_identity: default_federated_identity_interface(),
|
||||||
kms,
|
kms,
|
||||||
kms_runtime: default_kms_runtime_interface(),
|
kms_runtime: default_kms_runtime_interface(),
|
||||||
outbound_tls_runtime: default_outbound_tls_runtime_interface(),
|
outbound_tls_runtime: default_outbound_tls_runtime_interface(),
|
||||||
@@ -129,9 +130,7 @@ impl AppContext {
|
|||||||
iam: Arc<IamSys<ObjectStore>>,
|
iam: Arc<IamSys<ObjectStore>>,
|
||||||
kms: Arc<KmsServiceManager>,
|
kms: Arc<KmsServiceManager>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut context = Self::new(object_store, Arc::new(IamHandle::new(iam)), Arc::new(KmsHandle::new(kms)));
|
Self::new(object_store, Arc::new(IamHandle::new(iam)), Arc::new(KmsHandle::new(kms)))
|
||||||
context.oidc = oidc_interface(super::runtime_sources::oidc_handle());
|
|
||||||
context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn object_store(&self) -> Arc<ECStore> {
|
pub fn object_store(&self) -> Arc<ECStore> {
|
||||||
@@ -142,12 +141,12 @@ impl AppContext {
|
|||||||
self.iam.clone()
|
self.iam.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oidc(&self) -> Arc<dyn OidcInterface> {
|
pub fn federated_identity(&self) -> Arc<dyn FederatedIdentityInterface> {
|
||||||
self.oidc.clone()
|
self.federated_identity.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn publish_oidc_handle(&self, oidc: Arc<OidcSys>) -> bool {
|
pub fn publish_federated_identity_service(&self, service: Arc<FederatedIdentityService>) -> bool {
|
||||||
self.oidc.publish_handle(oidc)
|
self.federated_identity.publish_handle(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -276,7 +275,7 @@ impl AppContext {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(super) struct AppContextTestInterfaces {
|
pub(super) struct AppContextTestInterfaces {
|
||||||
pub(super) iam: Arc<dyn IamInterface>,
|
pub(super) iam: Arc<dyn IamInterface>,
|
||||||
pub(super) oidc: Arc<dyn OidcInterface>,
|
pub(super) federated_identity: Arc<dyn FederatedIdentityInterface>,
|
||||||
pub(super) kms: Arc<dyn KmsInterface>,
|
pub(super) kms: Arc<dyn KmsInterface>,
|
||||||
pub(super) kms_runtime: Arc<dyn KmsRuntimeInterface>,
|
pub(super) kms_runtime: Arc<dyn KmsRuntimeInterface>,
|
||||||
pub(super) outbound_tls_runtime: Arc<dyn OutboundTlsRuntimeInterface>,
|
pub(super) outbound_tls_runtime: Arc<dyn OutboundTlsRuntimeInterface>,
|
||||||
@@ -313,7 +312,7 @@ impl AppContext {
|
|||||||
Self {
|
Self {
|
||||||
object_store,
|
object_store,
|
||||||
iam: interfaces.iam,
|
iam: interfaces.iam,
|
||||||
oidc: interfaces.oidc,
|
federated_identity: interfaces.federated_identity,
|
||||||
kms: interfaces.kms,
|
kms: interfaces.kms,
|
||||||
kms_runtime: interfaces.kms_runtime,
|
kms_runtime: interfaces.kms_runtime,
|
||||||
outbound_tls_runtime: interfaces.outbound_tls_runtime,
|
outbound_tls_runtime: interfaces.outbound_tls_runtime,
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ use super::super::storage_api::context::runtime::{
|
|||||||
};
|
};
|
||||||
use super::interfaces::{
|
use super::interfaces::{
|
||||||
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BucketMonitorInterface, BufferConfigInterface,
|
ActionCredentialInterface, BootTimeInterface, BucketMetadataInterface, BucketMonitorInterface, BufferConfigInterface,
|
||||||
DeploymentIdInterface, EndpointsInterface, ExpiryStateInterface, IamInterface, InternodeMetricsInterface, KmsInterface,
|
DeploymentIdInterface, EndpointsInterface, ExpiryStateInterface, FederatedIdentityInterface, IamInterface,
|
||||||
KmsRuntimeInterface, LocalNodeNameInterface, LockClientInterface, LockClientsInterface, NotificationSystemInterface,
|
InternodeMetricsInterface, KmsInterface, KmsRuntimeInterface, LocalNodeNameInterface, LockClientInterface,
|
||||||
NotifyInterface, OidcInterface, OutboundTlsRuntimeInterface, PerformanceMetricsInterface, RegionInterface,
|
LockClientsInterface, NotificationSystemInterface, NotifyInterface, OutboundTlsRuntimeInterface, PerformanceMetricsInterface,
|
||||||
ReplicationPoolInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface, ScannerMetricsInterface,
|
RegionInterface, ReplicationPoolInterface, ReplicationStatsInterface, RuntimePortInterface, S3SelectDbInterface,
|
||||||
ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
|
ScannerMetricsInterface, ServerConfigInterface, StorageClassInterface, TierConfigInterface, TransitionStateInterface,
|
||||||
};
|
};
|
||||||
use super::runtime_sources;
|
use super::runtime_sources;
|
||||||
use crate::config::RustFSBufferConfig;
|
use crate::config::RustFSBufferConfig;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustfs_config::server_config::Config;
|
use rustfs_config::server_config::Config;
|
||||||
use rustfs_credentials::Credentials;
|
use rustfs_credentials::Credentials;
|
||||||
use rustfs_iam::{oidc::OidcSys, store::object::ObjectStore, sys::IamSys};
|
use rustfs_iam::{federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
|
||||||
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
||||||
use rustfs_kms::KmsServiceManager;
|
use rustfs_kms::KmsServiceManager;
|
||||||
use rustfs_lock::LockClient;
|
use rustfs_lock::LockClient;
|
||||||
@@ -42,7 +42,7 @@ use rustfs_tls_runtime::{GlobalPublishedOutboundTlsState, TlsGeneration};
|
|||||||
use s3s::dto::SelectObjectContentInput;
|
use s3s::dto::SelectObjectContentInput;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{Arc, RwLock as StdRwLock},
|
sync::{Arc, OnceLock, RwLock as StdRwLock},
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
@@ -76,35 +76,35 @@ impl IamInterface for IamHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default OIDC interface adapter.
|
/// Default federated identity service interface adapter.
|
||||||
pub struct OidcHandle {
|
pub struct FederatedIdentityHandle {
|
||||||
oidc: StdRwLock<Option<Arc<OidcSys>>>,
|
service: StdRwLock<Option<Arc<FederatedIdentityService>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OidcHandle {
|
impl FederatedIdentityHandle {
|
||||||
pub fn new(oidc: Option<Arc<OidcSys>>) -> Self {
|
pub fn new(service: Option<Arc<FederatedIdentityService>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
oidc: StdRwLock::new(oidc),
|
service: StdRwLock::new(service),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for OidcHandle {
|
impl Default for FederatedIdentityHandle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(None)
|
Self::new(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OidcInterface for OidcHandle {
|
impl FederatedIdentityInterface for FederatedIdentityHandle {
|
||||||
fn handle(&self) -> Option<Arc<OidcSys>> {
|
fn handle(&self) -> Option<Arc<FederatedIdentityService>> {
|
||||||
self.oidc.read().ok().and_then(|oidc| oidc.as_ref().cloned())
|
self.service.read().ok().and_then(|service| service.as_ref().cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publish_handle(&self, oidc: Arc<OidcSys>) -> bool {
|
fn publish_handle(&self, service: Arc<FederatedIdentityService>) -> bool {
|
||||||
let Ok(mut published_oidc) = self.oidc.write() else {
|
let Ok(mut published_service) = self.service.write() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
*published_oidc = Some(oidc);
|
*published_service = Some(service);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,12 +556,24 @@ pub fn default_action_credential_interface() -> Arc<dyn ActionCredentialInterfac
|
|||||||
Arc::new(ActionCredentialHandle::default())
|
Arc::new(ActionCredentialHandle::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_oidc_interface() -> Arc<dyn OidcInterface> {
|
static DEFAULT_FEDERATED_IDENTITY_HANDLE: OnceLock<Arc<FederatedIdentityHandle>> = OnceLock::new();
|
||||||
Arc::new(OidcHandle::default())
|
|
||||||
|
fn default_federated_identity_handle() -> Arc<FederatedIdentityHandle> {
|
||||||
|
DEFAULT_FEDERATED_IDENTITY_HANDLE
|
||||||
|
.get_or_init(|| Arc::new(FederatedIdentityHandle::default()))
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oidc_interface(oidc: Option<Arc<OidcSys>>) -> Arc<dyn OidcInterface> {
|
pub fn default_federated_identity_interface() -> Arc<dyn FederatedIdentityInterface> {
|
||||||
Arc::new(OidcHandle::new(oidc))
|
default_federated_identity_handle()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn publish_default_federated_identity_service(service: Arc<FederatedIdentityService>) -> bool {
|
||||||
|
default_federated_identity_handle().publish_handle(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn federated_identity_interface(service: Option<Arc<FederatedIdentityService>>) -> Arc<dyn FederatedIdentityInterface> {
|
||||||
|
Arc::new(FederatedIdentityHandle::new(service))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_region_interface() -> Arc<dyn RegionInterface> {
|
pub fn default_region_interface() -> Arc<dyn RegionInterface> {
|
||||||
@@ -594,10 +606,69 @@ pub fn default_buffer_config_interface() -> Arc<dyn BufferConfigInterface> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ServerConfigHandle, runtime_sources};
|
use super::{
|
||||||
|
ServerConfigHandle, default_federated_identity_interface, federated_identity_interface,
|
||||||
|
publish_default_federated_identity_service, runtime_sources,
|
||||||
|
};
|
||||||
use crate::app::context::interfaces::ServerConfigInterface;
|
use crate::app::context::interfaces::ServerConfigInterface;
|
||||||
use rustfs_config::server_config::Config;
|
use rustfs_config::server_config::Config;
|
||||||
|
use rustfs_iam::{
|
||||||
|
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
|
||||||
|
oidc::OidcSys,
|
||||||
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn test_federated_identity_service() -> Arc<FederatedIdentityService> {
|
||||||
|
let oidc = OidcSys::empty().expect("empty OIDC configuration should be valid");
|
||||||
|
let adapter = Arc::new(StandardOidcAdapter::new(Arc::new(oidc)));
|
||||||
|
Arc::new(FederatedIdentityService::new(FederatedIdentityRegistry::new(adapter)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn federated_identity_handle_preserves_early_publish_and_allows_replacement() {
|
||||||
|
let first = test_federated_identity_service();
|
||||||
|
let second = test_federated_identity_service();
|
||||||
|
let interface = federated_identity_interface(Some(first.clone()));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
interface
|
||||||
|
.handle()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|resolved| Arc::ptr_eq(resolved, &first))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(interface.publish_handle(second.clone()));
|
||||||
|
assert!(
|
||||||
|
interface
|
||||||
|
.handle()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|resolved| Arc::ptr_eq(resolved, &second))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_federated_identity_handle_shares_early_publish_and_replacement() {
|
||||||
|
let first = test_federated_identity_service();
|
||||||
|
let second = test_federated_identity_service();
|
||||||
|
|
||||||
|
assert!(publish_default_federated_identity_service(first.clone()));
|
||||||
|
let interface = default_federated_identity_interface();
|
||||||
|
assert!(
|
||||||
|
interface
|
||||||
|
.handle()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|resolved| Arc::ptr_eq(resolved, &first))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(publish_default_federated_identity_service(second.clone()));
|
||||||
|
assert!(
|
||||||
|
interface
|
||||||
|
.handle()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|resolved| Arc::ptr_eq(resolved, &second))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// backlog#1052 S3: each context's server-config handle keeps its own copy,
|
// backlog#1052 S3: each context's server-config handle keeps its own copy,
|
||||||
// so two handles that both published stay isolated even though the shared
|
// so two handles that both published stay isolated even though the shared
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use crate::config::RustFSBufferConfig;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustfs_config::server_config::Config;
|
use rustfs_config::server_config::Config;
|
||||||
use rustfs_credentials::Credentials;
|
use rustfs_credentials::Credentials;
|
||||||
use rustfs_iam::{oidc::OidcSys, store::object::ObjectStore, sys::IamSys};
|
use rustfs_iam::{federation::FederatedIdentityService, store::object::ObjectStore, sys::IamSys};
|
||||||
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics};
|
||||||
use rustfs_kms::KmsServiceManager;
|
use rustfs_kms::KmsServiceManager;
|
||||||
use rustfs_lock::LockClient;
|
use rustfs_lock::LockClient;
|
||||||
@@ -44,10 +44,10 @@ pub trait IamInterface: Send + Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OIDC interface for admin and runtime consumers.
|
/// Federated identity service interface for admin and runtime consumers.
|
||||||
pub trait OidcInterface: Send + Sync {
|
pub trait FederatedIdentityInterface: Send + Sync {
|
||||||
fn handle(&self) -> Option<Arc<OidcSys>>;
|
fn handle(&self) -> Option<Arc<FederatedIdentityService>>;
|
||||||
fn publish_handle(&self, _oidc: Arc<OidcSys>) -> bool {
|
fn publish_handle(&self, _service: Arc<FederatedIdentityService>) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use super::super::storage_api::context::runtime::{
|
|||||||
use crate::config::{RustFSBufferConfig, get_global_buffer_config};
|
use crate::config::{RustFSBufferConfig, get_global_buffer_config};
|
||||||
use rustfs_config::server_config::{Config, get_global_server_config, set_global_server_config};
|
use rustfs_config::server_config::{Config, get_global_server_config, set_global_server_config};
|
||||||
use rustfs_credentials::{Credentials, get_global_action_cred};
|
use rustfs_credentials::{Credentials, get_global_action_cred};
|
||||||
use rustfs_iam::oidc::OidcSys;
|
|
||||||
use rustfs_io_metrics::{
|
use rustfs_io_metrics::{
|
||||||
PerformanceMetrics,
|
PerformanceMetrics,
|
||||||
global_metrics::get_global_metrics,
|
global_metrics::get_global_metrics,
|
||||||
@@ -64,10 +63,6 @@ pub async fn outbound_tls_state() -> GlobalPublishedOutboundTlsState {
|
|||||||
load_global_outbound_tls_state().await
|
load_global_outbound_tls_state().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oidc_handle() -> Option<Arc<OidcSys>> {
|
|
||||||
rustfs_iam::get_oidc()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn token_signing_key() -> Option<String> {
|
pub fn token_signing_key() -> Option<String> {
|
||||||
rustfs_iam::manager::get_token_signing_key()
|
rustfs_iam::manager::get_token_signing_key()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ pub(crate) use context::{
|
|||||||
default_s3select_db_interface as fallback_s3select_db_interface,
|
default_s3select_db_interface as fallback_s3select_db_interface,
|
||||||
default_scanner_metrics_interface as fallback_scanner_metrics_interface,
|
default_scanner_metrics_interface as fallback_scanner_metrics_interface,
|
||||||
default_server_config_interface as fallback_server_config_interface,
|
default_server_config_interface as fallback_server_config_interface,
|
||||||
default_storage_class_interface as fallback_storage_class_interface, publish_oidc_handle, publish_server_config,
|
default_storage_class_interface as fallback_storage_class_interface, publish_federated_identity_service,
|
||||||
publish_storage_class_config, resolve_action_credentials as current_action_credentials,
|
publish_server_config, publish_storage_class_config, resolve_action_credentials as current_action_credentials,
|
||||||
resolve_boot_time as current_boot_time, resolve_bucket_metadata_handle as current_bucket_metadata_handle,
|
resolve_boot_time as current_boot_time, resolve_bucket_metadata_handle as current_bucket_metadata_handle,
|
||||||
resolve_bucket_monitor_handle as current_bucket_monitor_handle, resolve_buffer_config as current_buffer_config,
|
resolve_bucket_monitor_handle as current_bucket_monitor_handle, resolve_buffer_config as current_buffer_config,
|
||||||
resolve_daily_tier_stats as current_daily_tier_stats, resolve_deployment_id as current_deployment_id,
|
resolve_daily_tier_stats as current_daily_tier_stats, resolve_deployment_id as current_deployment_id,
|
||||||
resolve_encryption_service as current_encryption_service, resolve_endpoints_handle as current_endpoints_handle,
|
resolve_encryption_service as current_encryption_service, resolve_endpoints_handle as current_endpoints_handle,
|
||||||
resolve_expiry_state_handle as current_expiry_state_handle, resolve_iam_handle as current_iam_handle,
|
resolve_expiry_state_handle as current_expiry_state_handle,
|
||||||
|
resolve_federated_identity_service as current_federated_identity_service, resolve_iam_handle as current_iam_handle,
|
||||||
resolve_iam_ready as current_iam_ready, resolve_internode_metrics as current_internode_metrics,
|
resolve_iam_ready as current_iam_ready, resolve_internode_metrics as current_internode_metrics,
|
||||||
resolve_kms_runtime_service_manager as current_kms_runtime_service_manager,
|
resolve_kms_runtime_service_manager as current_kms_runtime_service_manager,
|
||||||
resolve_local_node_name as current_local_node_name, resolve_lock_client as current_lock_client,
|
resolve_local_node_name as current_local_node_name, resolve_lock_client as current_lock_client,
|
||||||
@@ -39,7 +40,6 @@ pub(crate) use context::{
|
|||||||
resolve_object_data_cache_handle_for_context as current_object_data_cache_handle_for_context,
|
resolve_object_data_cache_handle_for_context as current_object_data_cache_handle_for_context,
|
||||||
resolve_object_store_handle as current_object_store_handle,
|
resolve_object_store_handle as current_object_store_handle,
|
||||||
resolve_object_store_handle_for_context as current_object_store_handle_for_context,
|
resolve_object_store_handle_for_context as current_object_store_handle_for_context,
|
||||||
resolve_oidc_handle as current_oidc_handle,
|
|
||||||
resolve_or_init_kms_runtime_service_manager as current_or_init_kms_runtime_service_manager,
|
resolve_or_init_kms_runtime_service_manager as current_or_init_kms_runtime_service_manager,
|
||||||
resolve_outbound_tls_generation as current_outbound_tls_generation, resolve_outbound_tls_state as current_outbound_tls_state,
|
resolve_outbound_tls_generation as current_outbound_tls_generation, resolve_outbound_tls_state as current_outbound_tls_state,
|
||||||
resolve_performance_metrics as current_performance_metrics, resolve_ready_iam_handle as current_ready_iam_handle,
|
resolve_performance_metrics as current_performance_metrics, resolve_ready_iam_handle as current_ready_iam_handle,
|
||||||
|
|||||||
@@ -12,8 +12,14 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use rustfs_iam::{get_oidc, init_oidc_sys};
|
use rustfs_iam::{
|
||||||
use std::io::{Error, Result};
|
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
|
||||||
|
get_oidc, init_oidc_sys,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::{Error, Result},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
const LOG_COMPONENT_MAIN: &str = "main";
|
const LOG_COMPONENT_MAIN: &str = "main";
|
||||||
@@ -47,7 +53,10 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
|
|||||||
match init_oidc_sys().await {
|
match init_oidc_sys().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(oidc) = get_oidc() {
|
if let Some(oidc) = get_oidc() {
|
||||||
crate::runtime_sources::publish_oidc_handle(oidc);
|
let adapter = Arc::new(StandardOidcAdapter::new(oidc));
|
||||||
|
let registry = FederatedIdentityRegistry::new(adapter);
|
||||||
|
let service = Arc::new(FederatedIdentityService::new(registry));
|
||||||
|
crate::runtime_sources::publish_federated_identity_service(service);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user