From 70a2441407d475bd9b79b9567ccc68ccad7bec96 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Tue, 23 Jun 2026 20:05:28 +0800 Subject: [PATCH] refactor: route notify dispatch through app context (#3789) * refactor: route notify dispatch through app context * refactor: route admin IAM globals through app context (#3791) * refactor: centralize IAM root credential access (#3792) --- crates/iam/src/lib.rs | 5 + crates/iam/src/manager.rs | 8 +- crates/iam/src/root_credentials.rs | 31 +++++ crates/iam/src/store/object.rs | 10 +- crates/iam/src/sys.rs | 17 ++- crates/protocols/src/common/gateway.rs | 7 +- docs/architecture/migration-progress.md | 119 ++++++++++++++++-- rustfs/src/admin/console.rs | 3 +- rustfs/src/admin/handlers/oidc.rs | 12 +- rustfs/src/admin/handlers/site_replication.rs | 4 +- rustfs/src/admin/handlers/sts.rs | 12 +- rustfs/src/admin/handlers/table_catalog.rs | 6 +- rustfs/src/app/context.rs | 74 +++++++++-- rustfs/src/app/context/handles.rs | 8 ++ rustfs/src/app/context/interfaces.rs | 9 +- rustfs/src/init.rs | 6 +- rustfs/src/server/event.rs | 4 +- rustfs/src/storage/helper.rs | 6 +- scripts/layer-dependency-baseline.txt | 3 + 19 files changed, 272 insertions(+), 72 deletions(-) create mode 100644 crates/iam/src/root_credentials.rs diff --git a/crates/iam/src/lib.rs b/crates/iam/src/lib.rs index 7d93f6af7..80c968bdc 100644 --- a/crates/iam/src/lib.rs +++ b/crates/iam/src/lib.rs @@ -47,6 +47,7 @@ pub mod keyring; pub mod manager; pub mod oidc; pub mod oidc_state; +mod root_credentials; pub mod store; pub mod sys; pub mod utils; @@ -60,6 +61,10 @@ pub(crate) type IamStore = EcstoreStore; pub(crate) type IamConfigObjectInfo = ::ObjectInfo; pub(crate) type IamConfigObjectOptions = ::ObjectOptions; +pub fn is_root_access_key(access_key: &str) -> bool { + root_credentials::is_root_access_key(access_key) +} + pub(crate) async fn read_iam_config_no_lock(api: Arc, file: &str) -> IamStorageResult> { ecstore_read_config_no_lock(api, file).await } diff --git a/crates/iam/src/manager.rs b/crates/iam/src/manager.rs index f2dd215d4..c620da295 100644 --- a/crates/iam/src/manager.rs +++ b/crates/iam/src/manager.rs @@ -25,7 +25,7 @@ use crate::{ }, }; use futures::future::join_all; -use rustfs_credentials::{Credentials, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, get_global_action_cred}; +use rustfs_credentials::{Credentials, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE}; use rustfs_madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc}; use rustfs_policy::{ arn::ARN, @@ -2137,11 +2137,7 @@ fn set_default_canned_policies(policies: &mut HashMap) { } pub fn get_token_signing_key() -> Option { - if let Some(s) = get_global_action_cred() { - Some(s.secret_key) - } else { - None - } + crate::root_credentials::token_signing_key() } pub fn extract_jwt_claims(u: &UserIdentity) -> Result> { diff --git a/crates/iam/src/root_credentials.rs b/crates/iam/src/root_credentials.rs new file mode 100644 index 000000000..26c77f40a --- /dev/null +++ b/crates/iam/src/root_credentials.rs @@ -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 rustfs_credentials::{Credentials, get_global_action_cred}; + +pub(crate) fn credentials() -> Option { + get_global_action_cred() +} + +pub(crate) fn credentials_or_default() -> Credentials { + credentials().unwrap_or_default() +} + +pub(crate) fn token_signing_key() -> Option { + credentials().map(|cred| cred.secret_key) +} + +pub(crate) fn is_root_access_key(access_key: &str) -> bool { + credentials().is_some_and(|cred| cred.access_key == access_key) +} diff --git a/crates/iam/src/store/object.rs b/crates/iam/src/store/object.rs index fde9fca76..709ebc5c9 100644 --- a/crates/iam/src/store/object.rs +++ b/crates/iam/src/store/object.rs @@ -23,9 +23,9 @@ use crate::{ error::{is_err_no_such_policy, is_err_no_such_user}, keyring, manager::{extract_jwt_claims, extract_jwt_claims_allow_missing_exp, get_default_policyes}, + root_credentials, }; use futures::future::join_all; -use rustfs_credentials::get_global_action_cred; use rustfs_io_metrics::record_system_path_failure; use rustfs_policy::{auth::UserIdentity, policy::PolicyDoc}; use rustfs_storage_api::{HTTPPreconditions, ListOperations as _, ObjectInfoOrErr as StorageObjectInfoOrErr, ObjectOperations}; @@ -165,7 +165,7 @@ impl ObjectStore { } const STREAM_IO_HEADER_LEN: usize = 41; - let cred = get_global_action_cred().unwrap_or_default(); + let cred = root_credentials::credentials_or_default(); let mut last_err = None; let mut try_decrypt_with_key = |key: &[u8], source: DecryptSource| -> Option { @@ -1273,16 +1273,16 @@ impl Store for ObjectStore { mod tests { use super::{DecryptSource, ObjectStore}; use crate::keyring; - use rustfs_credentials::{Credentials, get_global_action_cred, init_global_action_credentials}; + use rustfs_credentials::{Credentials, init_global_action_credentials}; use serial_test::serial; use temp_env::with_vars; fn test_cred() -> Credentials { - if let Some(cred) = get_global_action_cred() { + if let Some(cred) = crate::root_credentials::credentials() { return cred; } let _ = init_global_action_credentials(Some("COMPATTESTAK".to_string()), Some("COMPATTESTSK1234567890".to_string())); - get_global_action_cred().unwrap_or_default() + crate::root_credentials::credentials_or_default() } #[test] diff --git a/crates/iam/src/sys.rs b/crates/iam/src/sys.rs index 86a603952..7c4dc4804 100644 --- a/crates/iam/src/sys.rs +++ b/crates/iam/src/sys.rs @@ -28,7 +28,7 @@ use crate::{ notify_iam_delete_policy, notify_iam_delete_service_account, notify_iam_delete_user, notify_iam_load_group, notify_iam_load_policy, notify_iam_load_policy_mapping, notify_iam_load_service_account, notify_iam_load_user, }; -use rustfs_credentials::{Credentials, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, get_global_action_cred}; +use rustfs_credentials::{Credentials, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE}; use rustfs_madmin::AddOrUpdateUserReq; use rustfs_madmin::GroupDesc; use rustfs_policy::arn::ARN; @@ -748,7 +748,7 @@ impl IamSys { } pub async fn check_key(&self, access_key: &str) -> Result<(Option, bool)> { - if let Some(sys_cred) = get_global_action_cred() + if let Some(sys_cred) = crate::root_credentials::credentials() && sys_cred.access_key == access_key { return Ok((Some(UserIdentity::new(sys_cred)), true)); @@ -1010,7 +1010,7 @@ impl IamSys { } pub(crate) async fn prepare_sts_auth(&self, args: &Args<'_>, parent_user: &str) -> PreparedIamAuth { - let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user); + let is_owner = crate::is_root_access_key(parent_user); let role_arn = args.get_role_arn(); let (effective_groups, groups_source, policies) = if is_owner { @@ -1162,7 +1162,7 @@ impl IamSys { && matches!(mode, PreparedServicePolicyMode::SessionBound) && matches!(session_policy, PreparedSessionPolicy::Policy(_)); - let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user); + let is_owner = crate::is_root_access_key(parent_user); let role_arn = args.get_role_arn(); let svc_policies = if is_owner || bypass_parent_policy { @@ -1352,7 +1352,7 @@ mod tests { use crate::error::Error; use crate::manager::get_default_policyes; use crate::store::{GroupInfo, MappedPolicy, Store, UserType}; - use rustfs_credentials::{Credentials, get_global_action_cred, init_global_action_credentials}; + use rustfs_credentials::{Credentials, init_global_action_credentials}; use rustfs_policy::auth::{UserIdentity, get_new_credentials_with_metadata}; use rustfs_policy::policy::Args; use rustfs_policy::policy::action::{Action, AdminAction, S3Action}; @@ -1660,7 +1660,7 @@ mod tests { } fn ensure_test_global_credentials() { - if get_global_action_cred().is_none() { + if crate::root_credentials::credentials().is_none() { let _ = init_global_action_credentials(Some("TESTROOTACCESSKEY".to_string()), Some("TESTROOTSECRET123".to_string())); } } @@ -1940,9 +1940,8 @@ mod tests { let iam_sys = IamSys::new(cache_manager); let parent_user = "sts-fallback-test-parent"; - let token_secret = get_global_action_cred() - .expect("global action credentials should be initialized") - .secret_key; + let token_secret = crate::root_credentials::token_signing_key() + .unwrap_or_else(|| unreachable!("global action credentials should be initialized")); let mut claims = HashMap::new(); claims.insert("parent".to_string(), Value::String(parent_user.to_string())); claims.insert( diff --git a/crates/protocols/src/common/gateway.rs b/crates/protocols/src/common/gateway.rs index 5a7f13266..28e2db992 100644 --- a/crates/protocols/src/common/gateway.rs +++ b/crates/protocols/src/common/gateway.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use rustfs_credentials; use rustfs_policy::policy::action::S3Action as PolicyS3Action; use serde_json; use std::collections::HashMap; @@ -299,11 +298,7 @@ pub async fn is_authorized( let policy_action: rustfs_policy::policy::action::Action = action.clone().into(); // Check if user is the owner (admin) - let is_owner = if let Some(global_cred) = rustfs_credentials::get_global_action_cred() { - session_context.principal.access_key() == global_cred.access_key - } else { - false - }; + let is_owner = rustfs_iam::is_root_access_key(session_context.principal.access_key()); let args = rustfs_policy::policy::Args { account: session_context.principal.access_key(), diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index b862a4a60..5f31363e6 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,16 +5,18 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Current Context - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) -- Branch: `overtrue/arch-iam-global-read-batch` -- Baseline: completed `C-011/C-012/C-013/API-055/API-059/API-079/API-080/API-081/API-082/API-083/API-084/API-085/API-086/API-087/API-088/API-089/API-090/API-091/API-092/API-093/API-094/API-095/API-096/API-097/API-098/API-099/API-100/API-101/API-102/API-103/API-104/API-105/API-106/API-107/API-108/API-109/API-110/API-111/API-112/API-113/API-114/API-115/API-116/API-117/API-118/API-119/API-120/API-121/API-122/API-123/API-124/API-125/API-126/API-127/API-128/API-129/API-130/API-131/API-132/API-133/API-134/API-135/API-136/API-137/API-138/API-139/API-140/API-141/API-142/API-143/API-144/API-145/API-146/API-147/API-148/API-149/API-150/API-151/API-152/API-153/API-154/API-155/API-156/API-157/API-158/API-159/API-160/API-161/API-162/API-163/API-164/API-165/API-166/API-167/API-168/API-169/API-170/API-171/API-172/API-173/API-174/API-175/API-176/API-177/API-178`. -- Based on: API-171 through API-177 prepared in PR #3785; this branch batches - the next IAM consumer migration on top of that branch. +- Branch: `overtrue/arch-iam-credential-boundary` +- Baseline: completed `C-011/C-012/C-013/API-055/API-059/API-079/API-080/API-081/API-082/API-083/API-084/API-085/API-086/API-087/API-088/API-089/API-090/API-091/API-092/API-093/API-094/API-095/API-096/API-097/API-098/API-099/API-100/API-101/API-102/API-103/API-104/API-105/API-106/API-107/API-108/API-109/API-110/API-111/API-112/API-113/API-114/API-115/API-116/API-117/API-118/API-119/API-120/API-121/API-122/API-123/API-124/API-125/API-126/API-127/API-128/API-129/API-130/API-131/API-132/API-133/API-134/API-135/API-136/API-137/API-138/API-139/API-140/API-141/API-142/API-143/API-144/API-145/API-146/API-147/API-148/API-149/API-150/API-151/API-152/API-153/API-154/API-155/API-156/API-157/API-158/API-159/API-160/API-161/API-162/API-163/API-164/API-165/API-166/API-167/API-168/API-169/API-170/API-171/API-172/API-173/API-174/API-175/API-176/API-177/API-178/API-179/API-180/API-181`. +- Based on: API-180 prepared in PR #3791; this branch batches IAM root + credential boundary cleanup on top of that branch. - PR type for this branch: `consumer-migration` - Runtime behavior changes: none. - Rust code changes: route replication pool, outbound TLS generation, runtime region, KMS encryption service, runtime support handles, S3 Select DB, - internode RPC metrics, and IAM authorization/handler reads through - AppContext-first resolvers. + internode RPC metrics, IAM authorization/handler reads, notification + rules/event dispatch, admin OIDC/token-signing reads, and IAM root + credential consumers through AppContext-first or IAM-owned resolver + boundaries. - CI/script changes: lock completed owner and test/fuzz boundaries against bare/glob imports, scattered raw ECStore facade subpaths, and startup runtime/root-server/table/S3/app shared/app bucket/app ECStore/admin facade @@ -23,7 +25,7 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block and storage owner thin bridge regressions, plus app context and notify event-bridge thin module regressions; accept the reviewed AppContext resolver reverse dependencies in the layer baseline. -- Docs changes: record the API-136 through API-178 owner facade cleanup. +- Docs changes: record the API-136 through API-181 owner facade cleanup. ## Phase 0 Tasks @@ -4571,6 +4573,52 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block migration guard, layer guard, formatting, diff hygiene, residual IAM getter scan, Rust risk scan, branch freshness check, and three-expert review. +- [x] `API-179` Route notification dispatch through AppContext. + - Do: route startup notification rule registration, storage event + notifications, and ECStore event dispatch hooks through the AppContext + notify interface. + - Acceptance: production RustFS notification dispatch paths no longer call + the notify global directly, while the default adapter preserves the legacy + notifier fallback. + - Must preserve: bucket notification rule registration, operation helper + success-only event emission, replication-request suppression, ECStore event + conversion, and background spawn behavior. + - Verification: RustFS compile coverage, targeted context resolver tests, + migration guard, layer guard, formatting, diff hygiene, residual notify + dispatch scan, Rust risk scan, branch freshness check, and three-expert + review. + +- [x] `API-180` Route admin OIDC and token-signing reads through AppContext. + - Do: expose AppContext-first resolvers for the OIDC system and token + signing key, then route console config, OIDC handlers, STS credential + generation, table credential vending, and site-replication STS validation + through those resolvers. + - Acceptance: targeted RustFS admin OIDC/token-signing consumers no longer + read IAM globals directly, while default adapters preserve the existing + global OIDC and action-credential fallback. + - Must preserve: OIDC provider listing, authorize/callback/logout behavior, + STS web-identity verification, STS/table credential claims, session-policy + handling, and site-replication STS token validation. + - Verification: RustFS compile coverage, targeted context resolver tests, + migration guard, layer guard, formatting, diff hygiene, residual + admin/global IAM scan, Rust risk scan, branch freshness check, and + three-expert review. + +- [x] `API-181` Centralize IAM root credential reads behind IAM boundary. + - Do: add an IAM-owned root credential helper, route IAM store/sys/token + signing consumers through it, and make protocol gateway owner checks call an + IAM predicate instead of reading credentials directly. + - Acceptance: production IAM and protocol gateway paths no longer call the + action credential global directly outside the IAM boundary module, while + root user detection, legacy IAM decrypt fallback, and token-signing behavior + remain unchanged. + - Must preserve: root credential lookup, owner-policy bypass decisions, + legacy secret-key decrypt fallback, STS token signing key selection, + service-account/STSes authorization, and protocol gateway policy args. + - Verification: IAM/protocol compile coverage, IAM focused tests, formatting, + migration guard, layer guard, diff hygiene, residual direct credential scan, + Rust risk scan, branch freshness check, and three-expert review. + ## Next PRs 1. `consumer-migration`: continue reducing direct global reads behind AppContext resolver boundaries. @@ -4661,11 +4709,68 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block | Quality/architecture | pass | API-178 keeps ready IAM access behind an AppContext-first resolver without widening handler semantics. | | Migration preservation | pass | Auth, storage authorization, admin IAM handlers, STS, and table credential flows keep existing error mapping and ready-check fallback. | | Testing/verification | pass | RustFS focused compile, targeted context resolver test, formatting, migration/layer guards, diff hygiene, residual IAM getter scan, Rust risk scan, and pre-commit passed for API-178. | +| Quality/architecture | pass | API-179 keeps notification rule registration and event dispatch behind the AppContext notify resolver. | +| Migration preservation | pass | Startup rules, success-only storage events, replication suppression, ECStore hook conversion, and default notifier fallback are preserved. | +| Testing/verification | pass | RustFS focused compile, targeted context resolver test, formatting, migration/layer guards, diff hygiene, residual notify dispatch scan, Rust risk scan, and pre-commit passed for API-179. | +| Quality/architecture | pass | API-180 keeps admin OIDC and token-signing reads behind IAM/AppContext resolver methods without widening handler behavior. | +| Migration preservation | pass | OIDC provider discovery, STS credential generation, table credential vending, and site-replication STS validation keep existing error mapping and fallback semantics. | +| Testing/verification | pass | RustFS focused compile, targeted context resolver test, formatting/migration/layer guards, diff hygiene, residual admin IAM scan, and Rust risk scan passed for API-180. | +| Quality/architecture | pass | API-181 keeps IAM root credential reads centralized in an IAM-owned helper and exposes only a root access-key predicate to protocol code. | +| Migration preservation | pass | Root owner checks, legacy IAM decrypt fallback, token signing, and gateway policy args preserve existing credential semantics. | +| Testing/verification | pass | IAM/protocol compile, IAM unit tests, formatting, migration/layer guards, diff hygiene, residual credential scan, and Rust risk scan passed for API-181. | ## Verification Notes Passed before push: +- Issue #660 API-181 current slice: + - `cargo check -p rustfs-iam -p rustfs-protocols --tests`: passed. + - `cargo test -p rustfs-iam --lib`: passed. + - `cargo fmt --all`: passed. + - `cargo fmt --all --check`: passed. + - `git diff --check`: passed. + - `bash -n scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_layer_dependencies.sh`: passed. + - IAM/protocol credential scan: passed; production direct + `get_global_action_cred` calls are isolated to the IAM root credential + boundary. + - Rust risk scan: no new production unwrap/expect, panic/todo/unsafe, or cast + risks added. + - `make pre-commit`: passed. + +- Issue #660 API-180 current slice: + - `cargo check --tests -p rustfs`: passed. + - `cargo test -p rustfs resolver_helpers_are_context_first_and_fallback_when_context_is_absent --lib`: + passed. + - `cargo fmt --all`: passed. + - `cargo fmt --all --check`: passed. + - `git diff --check`: passed. + - `bash -n scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_layer_dependencies.sh`: passed. + - Admin IAM/OIDC scan: passed; targeted admin OIDC and token-signing reads + now go through AppContext IAM resolvers. + - Rust risk scan: no new production unwrap/expect, panic/todo/unsafe, or cast + risks added. + - `make pre-commit`: passed. + +- Issue #660 API-179 current slice: + - `cargo check --tests -p rustfs`: passed. + - `cargo test -p rustfs resolver_helpers_are_context_first_and_fallback_when_context_is_absent --lib`: + passed. + - `cargo fmt --all`: passed. + - `cargo fmt --all --check`: passed. + - `git diff --check`: passed. + - `bash -n scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_layer_dependencies.sh`: passed. + - Notify dispatch scan: passed; production startup, storage helper, and + ECStore event hook dispatch no longer call the notify global directly. + - Rust risk scan: no new production unwrap/expect, panic/todo/unsafe, or cast + risks added. + - `make pre-commit`: passed. + - Issue #660 API-178 current slice: - `cargo check --tests -p rustfs`: passed. - `cargo test -p rustfs resolver_helpers_are_context_first_and_fallback_when_context_is_absent --lib`: diff --git a/rustfs/src/admin/console.rs b/rustfs/src/admin/console.rs index 55314341d..65af8f52d 100644 --- a/rustfs/src/admin/console.rs +++ b/rustfs/src/admin/console.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::admin::handlers::health::{HealthProbe, build_health_response_parts, collect_dependency_readiness}; +use crate::app::context::resolve_oidc_handle; use crate::license::has_valid_license; use crate::server::has_path_prefix; use crate::server::{ @@ -134,7 +135,7 @@ impl Config { let http_prefix = rustfs_config::RUSTFS_HTTP_PREFIX; // Collect OIDC provider info if available - let oidc = rustfs_iam::get_oidc() + let oidc = resolve_oidc_handle() .map(|sys| { sys.list_visible_providers() .into_iter() diff --git a/rustfs/src/admin/handlers/oidc.rs b/rustfs/src/admin/handlers/oidc.rs index 5c35e0abd..d1b0acdcb 100644 --- a/rustfs/src/admin/handlers/oidc.rs +++ b/rustfs/src/admin/handlers/oidc.rs @@ -16,7 +16,7 @@ use super::super::{read_admin_config_without_migrate, save_admin_server_config}; use super::sts::create_oidc_sts_credentials; use crate::admin::auth::validate_admin_request; use crate::admin::router::{AdminOperation, Operation, S3Router}; -use crate::app::context::{resolve_object_store_handle, resolve_server_config}; +use crate::app::context::{resolve_object_store_handle, resolve_oidc_handle, resolve_server_config}; use crate::auth::{check_key_valid, get_session_token}; use crate::server::{ADMIN_PREFIX, MINIO_ADMIN_PREFIX, RemoteAddr}; use http::StatusCode; @@ -268,7 +268,7 @@ pub struct ListOidcProvidersHandler {} #[async_trait::async_trait] impl Operation for ListOidcProvidersHandler { async fn call(&self, _req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let oidc_sys = rustfs_iam::get_oidc().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; + let oidc_sys = resolve_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; let providers = oidc_sys.list_visible_providers(); let json_body = serde_json::to_vec(&providers) @@ -439,7 +439,7 @@ impl Operation for OidcAuthorizeHandler { return Err(s3_error!(InvalidRequest, "invalid provider_id")); } - let oidc_sys = rustfs_iam::get_oidc().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; + let oidc_sys = resolve_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; // Derive the callback redirect URI from the request let redirect_uri = derive_callback_uri(&req, provider_id)?; @@ -512,7 +512,7 @@ impl Operation for OidcCallbackHandler { )); } - let oidc_sys = rustfs_iam::get_oidc().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; + let oidc_sys = resolve_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; let redirect_uri = derive_callback_uri(&req, provider_id)?; @@ -599,7 +599,7 @@ impl Operation for OidcLogoutHandler { return redirect_response(&fallback_location); }; - let location = match rustfs_iam::get_oidc() { + let location = match resolve_oidc_handle() { Some(oidc_sys) => match oidc_sys.build_logout_url(&logout_token, &fallback_location).await { Ok(Some(url)) => url, Ok(None) => fallback_location.clone(), @@ -628,7 +628,7 @@ impl Operation for OidcLogoutHandler { /// an explicit redirect_uri is recommended to prevent header manipulation. fn derive_callback_uri(req: &S3Request, provider_id: &str) -> S3Result { // Use explicitly configured redirect_uri if available - if let Some(oidc_sys) = rustfs_iam::get_oidc() + if let Some(oidc_sys) = resolve_oidc_handle() && let Some(config) = oidc_sys.get_provider_config(provider_id) { if let Some(ref uri) = config.redirect_uri { diff --git a/rustfs/src/admin/handlers/site_replication.rs b/rustfs/src/admin/handlers/site_replication.rs index 7c9b7bfb7..bce6e7785 100644 --- a/rustfs/src/admin/handlers/site_replication.rs +++ b/rustfs/src/admin/handlers/site_replication.rs @@ -34,7 +34,7 @@ use crate::admin::utils::{encode_compatible_admin_payload, read_compatible_admin use crate::app::context::{ resolve_deployment_id, resolve_endpoints_handle, resolve_iam_handle, resolve_object_store_handle, resolve_oidc_handle, resolve_outbound_tls_generation, resolve_outbound_tls_state, resolve_region, resolve_replication_pool_handle, - resolve_replication_stats_handle, resolve_runtime_port, resolve_server_config, + resolve_replication_stats_handle, resolve_runtime_port, resolve_server_config, resolve_token_signing_key, }; use crate::auth::{check_key_valid, get_session_token}; use crate::config::get_config_snapshot; @@ -3556,7 +3556,7 @@ async fn apply_iam_item(item: SRIAMItem) -> S3Result<()> { let Some(sts_credential) = item.sts_credential else { return Err(s3_error!(InvalidRequest, "stsCredential is required")); }; - let Some(secret) = rustfs_iam::manager::get_token_signing_key() else { + let Some(secret) = resolve_token_signing_key() else { return Err(s3_error!(InvalidRequest, "token signing key not initialized")); }; let claims = get_claims_from_token_with_secret(&sts_credential.session_token, &secret) diff --git a/rustfs/src/admin/handlers/sts.rs b/rustfs/src/admin/handlers/sts.rs index 24ea684f3..9b608b2b9 100644 --- a/rustfs/src/admin/handlers/sts.rs +++ b/rustfs/src/admin/handlers/sts.rs @@ -19,7 +19,7 @@ use crate::{ handlers::site_replication::site_replication_iam_change_hook, router::{AdminOperation, Operation, S3Router}, }, - app::context::resolve_action_credentials, + app::context::{resolve_action_credentials, resolve_oidc_handle, resolve_token_signing_key}, auth::{check_key_valid, extract_string_list_claim, get_session_token}, server::ADMIN_PREFIX, server::RemoteAddr, @@ -29,7 +29,7 @@ use http::header::HeaderValue; use hyper::Method; use matchit::Params; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; -use rustfs_iam::{manager::get_token_signing_key, oidc::OidcClaims, sys::SESSION_POLICY_NAME}; +use rustfs_iam::{oidc::OidcClaims, sys::SESSION_POLICY_NAME}; use rustfs_madmin::{SITE_REPL_API_VERSION, SRIAMItem, SRSTSCredential}; use rustfs_policy::{ auth::get_new_credentials_with_metadata, @@ -59,7 +59,7 @@ fn has_identity_authorization_context(policies: &[String], groups: &[String]) -> } fn configured_roles_claim_key(provider_id: &str) -> Option { - rustfs_iam::get_oidc() + resolve_oidc_handle() .as_ref() .and_then(|oidc_sys| oidc_sys.get_provider_config(provider_id)) .map(|cfg| cfg.roles_claim.trim().to_string()) @@ -239,7 +239,7 @@ async fn handle_assume_role( return Err(s3_error!(InvalidArgument, "invalid policy arg")); } - let Some(secret) = get_token_signing_key() else { + let Some(secret) = resolve_token_signing_key() else { return Err(s3_error!(InvalidArgument, "global active sk not init")); }; @@ -316,7 +316,7 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu } // Verify the JWT and extract claims - let oidc_sys = rustfs_iam::get_oidc().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; + let oidc_sys = resolve_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) @@ -429,7 +429,7 @@ pub async fn create_oidc_sts_credentials( } // Generate STS temp credentials - let secret = get_token_signing_key().ok_or_else(|| s3_error!(InternalError, "token signing key not initialized"))?; + let secret = resolve_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}")))?; diff --git a/rustfs/src/admin/handlers/table_catalog.rs b/rustfs/src/admin/handlers/table_catalog.rs index 0aca5ce98..6e8cb743a 100644 --- a/rustfs/src/admin/handlers/table_catalog.rs +++ b/rustfs/src/admin/handlers/table_catalog.rs @@ -17,7 +17,7 @@ use crate::admin::{ auth::{AdminResourceScope, validate_admin_request, validate_admin_request_with_bucket_object}, router::{AdminOperation, Operation, S3Router}, }; -use crate::app::context::resolve_object_store_handle; +use crate::app::context::{resolve_object_store_handle, resolve_token_signing_key}; use crate::auth::{check_key_valid, get_session_token}; use crate::server::{RemoteAddr, TABLE_CATALOG_COMPAT_PREFIX, TABLE_CATALOG_PREFIX}; use crate::table_catalog::{DEFAULT_WAREHOUSE_ID, TableCatalogStore}; @@ -26,7 +26,7 @@ use hyper::Method; use matchit::Params; use metrics::{counter, histogram}; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; -use rustfs_iam::{manager::get_token_signing_key, sys::SESSION_POLICY_NAME}; +use rustfs_iam::sys::SESSION_POLICY_NAME; use rustfs_policy::{ auth::get_new_credentials_with_metadata, policy::{ @@ -696,7 +696,7 @@ impl TableCredentialIssuer for IamTableCredentialIssuer { serde_json::Value::String(request.scope_prefix.clone()), ); - let secret = get_token_signing_key().ok_or_else(|| s3_error!(InternalError, "token signing key not initialized"))?; + let secret = resolve_token_signing_key().ok_or_else(|| s3_error!(InternalError, "token signing key not initialized"))?; let mut credential = get_new_credentials_with_metadata(&claims, &secret) .map_err(|err| s3_error!(InternalError, "failed to generate table credentials: {}", err))?; bind_table_credential_parent(&mut credential, principal); diff --git a/rustfs/src/app/context.rs b/rustfs/src/app/context.rs index ffdc92c15..4b6f1c30d 100644 --- a/rustfs/src/app/context.rs +++ b/rustfs/src/app/context.rs @@ -37,8 +37,7 @@ use super::{BucketBandwidthMonitor, DynReplicationPool, NotificationSys, Replica use crate::config::RustFSBufferConfig; use rustfs_config::server_config::Config; use rustfs_credentials::Credentials; -use rustfs_iam::oidc::OidcSys; -use rustfs_iam::{error::Error as IamError, store::object::ObjectStore, sys::IamSys}; +use rustfs_iam::{error::Error as IamError, oidc::OidcSys, store::object::ObjectStore, sys::IamSys}; use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics}; use rustfs_kms::{KmsServiceManager, ObjectEncryptionService, init_global_kms_service_manager}; use rustfs_lock::LockClient; @@ -85,14 +84,19 @@ pub fn resolve_iam_handle() -> Option>> { resolve_iam_handle_with(get_global_app_context(), rustfs_iam::get_global_iam_sys) } -/// Resolve OIDC handle using AppContext-first precedence. +/// Resolve a ready IAM system handle using AppContext-first precedence. +pub fn resolve_ready_iam_handle() -> rustfs_iam::error::Result>> { + resolve_ready_iam_handle_with(get_global_app_context(), rustfs_iam::get) +} + +/// Resolve OIDC system handle using AppContext-first precedence. pub fn resolve_oidc_handle() -> Option> { resolve_oidc_handle_with(get_global_app_context(), rustfs_iam::get_oidc) } -/// Resolve a ready IAM system handle using AppContext-first precedence. -pub fn resolve_ready_iam_handle() -> rustfs_iam::error::Result>> { - resolve_ready_iam_handle_with(get_global_app_context(), rustfs_iam::get) +/// Resolve token signing key using AppContext-first precedence. +pub fn resolve_token_signing_key() -> Option { + resolve_token_signing_key_with(get_global_app_context(), rustfs_iam::manager::get_token_signing_key) } /// Resolve bucket metadata handle using AppContext-first precedence. @@ -111,6 +115,12 @@ pub fn resolve_object_store_handle_for_context(context: Option<&AppContext>) -> context.map(|context| context.object_store()).or_else(new_object_layer_fn) } +/// Resolve notify interface using AppContext-first precedence. +pub fn resolve_notify_interface() -> Arc { + let context = get_global_app_context(); + resolve_notify_interface_for_context(context.as_deref()) +} + /// Resolve notify interface using an explicit AppContext, falling back to the legacy global notifier. pub fn resolve_notify_interface_for_context(context: Option<&AppContext>) -> Arc { context @@ -323,6 +333,12 @@ fn resolve_ready_iam_handle_with( fallback() } +fn resolve_token_signing_key_with(context: Option>, fallback: impl FnOnce() -> Option) -> Option { + context + .and_then(|context| context.iam().token_signing_key()) + .or_else(fallback) +} + fn resolve_bucket_metadata_handle_with( context: Option>, fallback: impl FnOnce() -> Option>>, @@ -546,7 +562,7 @@ mod tests { }; use crate::config::{RustFSBufferConfig, WorkloadProfile}; use async_trait::async_trait; - use rustfs_iam::{store::object::ObjectStore, sys::IamSys}; + use rustfs_iam::{oidc::OidcSys, store::object::ObjectStore, sys::IamSys}; use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics}; use rustfs_lock::{LocalClient, LockClient}; use rustfs_s3select_api::{ @@ -562,6 +578,8 @@ mod tests { struct TestIamInterface { ready: bool, + oidc: Option>, + token_signing_key: Option, } impl IamInterface for TestIamInterface { @@ -572,13 +590,23 @@ mod tests { fn is_ready(&self) -> bool { self.ready } + + fn oidc(&self) -> Option> { + self.oidc.clone() + } + + fn token_signing_key(&self) -> Option { + self.token_signing_key.clone() + } } - struct TestOidcInterface; + struct TestOidcInterface { + oidc: Option>, + } impl OidcInterface for TestOidcInterface { fn handle(&self) -> Option> { - None + self.oidc.clone() } } @@ -985,12 +1013,30 @@ mod tests { }; let context_region: s3s::region::Region = "context-region".parse().expect("test region"); let fallback_region: s3s::region::Region = "fallback-region".parse().expect("test region"); + let context_oidc_sys = match OidcSys::empty() { + Ok(sys) => sys, + Err(err) => unreachable!("test OIDC sys should initialize: {err}"), + }; + let fallback_oidc_sys = match OidcSys::empty() { + Ok(sys) => sys, + Err(err) => unreachable!("test OIDC fallback sys should initialize: {err}"), + }; + let context_oidc = Arc::new(context_oidc_sys); + let fallback_oidc = Arc::new(fallback_oidc_sys); + let context_token_signing_key = "context-token-signing-key".to_string(); + let fallback_token_signing_key = "fallback-token-signing-key".to_string(); let context = Arc::new(AppContext::with_test_interfaces( object_store.clone(), AppContextTestInterfaces { - iam: Arc::new(TestIamInterface { ready: true }), - oidc: Arc::new(TestOidcInterface), + iam: Arc::new(TestIamInterface { + ready: true, + oidc: None, + token_signing_key: Some(context_token_signing_key.clone()), + }), + oidc: Arc::new(TestOidcInterface { + oidc: Some(context_oidc.clone()), + }), kms: Arc::new(TestKmsInterface { kms: context_kms.clone(), }), @@ -1084,6 +1130,12 @@ mod tests { context_outbound_tls_state.generation ); assert!(resolve_iam_ready_with(Some(context.clone()), || false)); + let resolved_oidc = resolve_oidc_handle_with(Some(context.clone()), || Some(fallback_oidc.clone())); + assert!(resolved_oidc.as_ref().is_some_and(|oidc| Arc::ptr_eq(oidc, &context_oidc))); + assert_eq!( + resolve_token_signing_key_with(Some(context.clone()), || Some(fallback_token_signing_key.clone())).as_deref(), + Some(context_token_signing_key.as_str()) + ); assert!(Arc::ptr_eq( &resolve_bucket_metadata_handle_with(Some(context.clone()), || None).expect("context bucket metadata"), &bucket_metadata diff --git a/rustfs/src/app/context/handles.rs b/rustfs/src/app/context/handles.rs index 6e3d81452..47aec04eb 100644 --- a/rustfs/src/app/context/handles.rs +++ b/rustfs/src/app/context/handles.rs @@ -74,6 +74,14 @@ impl IamInterface for IamHandle { fn is_ready(&self) -> bool { rustfs_iam::get().is_ok() } + + fn oidc(&self) -> Option> { + rustfs_iam::get_oidc() + } + + fn token_signing_key(&self) -> Option { + rustfs_iam::manager::get_token_signing_key() + } } /// Default OIDC interface adapter. diff --git a/rustfs/src/app/context/interfaces.rs b/rustfs/src/app/context/interfaces.rs index 207576e3e..81e9cd164 100644 --- a/rustfs/src/app/context/interfaces.rs +++ b/rustfs/src/app/context/interfaces.rs @@ -23,8 +23,7 @@ use crate::config::RustFSBufferConfig; use async_trait::async_trait; use rustfs_config::server_config::Config; use rustfs_credentials::Credentials; -use rustfs_iam::oidc::OidcSys; -use rustfs_iam::{store::object::ObjectStore, sys::IamSys}; +use rustfs_iam::{oidc::OidcSys, store::object::ObjectStore, sys::IamSys}; use rustfs_io_metrics::{PerformanceMetrics, internode_metrics::InternodeMetrics}; use rustfs_kms::KmsServiceManager; use rustfs_lock::LockClient; @@ -41,6 +40,12 @@ pub trait IamInterface: Send + Sync { #[allow(dead_code)] fn handle(&self) -> Arc>; fn is_ready(&self) -> bool; + fn oidc(&self) -> Option> { + None + } + fn token_signing_key(&self) -> Option { + None + } } /// OIDC interface for admin and runtime consumers. diff --git a/rustfs/src/init.rs b/rustfs/src/init.rs index 43f31df51..42000f710 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::app::context::resolve_region; +use crate::app::context::{resolve_notify_interface, resolve_region}; use crate::server::ShutdownHandle; use crate::storage::{ get_bucket_notification_config, process_lambda_configurations, process_queue_configurations, process_topic_configurations, @@ -22,7 +22,6 @@ use rustfs_config::{ DEFAULT_BUFFER_MAX_SIZE, DEFAULT_BUFFER_MIN_SIZE, DEFAULT_BUFFER_PROFILE, DEFAULT_BUFFER_UNKNOWN_SIZE, DEFAULT_UPDATE_CHECK, ENV_RUSTFS_BUFFER_DEFAULT_SIZE, ENV_RUSTFS_BUFFER_MAX_SIZE, ENV_RUSTFS_BUFFER_MIN_SIZE, ENV_UPDATE_CHECK, RUSTFS_REGION, }; -use rustfs_notify::notifier_global; use rustfs_targets::arn::{ARN, TargetIDError}; use rustfs_utils::get_env_usize; use s3s::s3_error; @@ -245,7 +244,8 @@ pub async fn add_bucket_notification_configuration(buckets: Vec) { ); } - if let Err(e) = notifier_global::add_event_specific_rules(bucket, region, &event_rules) + if let Err(e) = resolve_notify_interface() + .add_event_specific_rules(bucket, region, &event_rules) .await .map_err(|e| s3_error!(InternalError, "Failed to add rules: {e}")) { diff --git a/rustfs/src/server/event.rs b/rustfs/src/server/event.rs index b5b044a26..00f91dce5 100644 --- a/rustfs/src/server/event.rs +++ b/rustfs/src/server/event.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::{module_switch::resolve_notify_module_state, refresh_persisted_module_switches_from_store}; -use crate::app::context::resolve_server_config; +use crate::app::context::{resolve_notify_interface, resolve_server_config}; use crate::storage::{EventArgs as EcstoreEventArgs, StorageObjectInfo, register_event_dispatch_hook}; use chrono::{DateTime, Utc}; use rustfs_notify::{EventArgs as NotifyEventArgs, NotifyObjectInfo}; @@ -118,7 +118,7 @@ fn install_ecstore_event_dispatch_hook() { return; }; spawn(async move { - rustfs_notify::notifier_global::notify(notify_args).await; + resolve_notify_interface().notify(notify_args).await; }); }); diff --git a/rustfs/src/storage/helper.rs b/rustfs/src/storage/helper.rs index db84fda05..98c4f94a4 100644 --- a/rustfs/src/storage/helper.rs +++ b/rustfs/src/storage/helper.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::app::context::resolve_action_credentials; +use crate::app::context::{resolve_action_credentials, resolve_notify_interface}; use crate::server::{convert_ecstore_object_info, is_audit_module_enabled, is_notify_module_enabled}; use crate::storage::access::{ReqInfo, request_context_from_req}; use crate::storage::request_context::{RequestContext, extract_request_id_from_headers}; @@ -24,7 +24,7 @@ use rustfs_audit::{ global::AuditLogger, }; use rustfs_io_metrics::record_s3_op; -use rustfs_notify::{EventArgsBuilder, notifier_global}; +use rustfs_notify::EventArgsBuilder; use rustfs_s3_ops::{S3Operation, operation_matches_event_name}; use rustfs_s3_types::EventName; use rustfs_targets::{ @@ -383,7 +383,7 @@ impl Drop for OperationHelper { if !event_args.is_replication_request() { let ctx = state.request_context.clone(); spawn_background_with_context(ctx, async move { - notifier_global::notify(event_args).await; + resolve_notify_interface().notify(event_args).await; }); } } diff --git a/scripts/layer-dependency-baseline.txt b/scripts/layer-dependency-baseline.txt index 571c1ad90..64b8e81a7 100644 --- a/scripts/layer-dependency-baseline.txt +++ b/scripts/layer-dependency-baseline.txt @@ -41,11 +41,13 @@ dep|rustfs/src/app/object_usecase.rs|app->interface|crate::storage::ecfs dep|rustfs/src/app/object_usecase.rs|app->interface|crate::storage::s3_api::multipart::parse_list_parts_params dep|rustfs/src/auth.rs|infra->app|crate::app::context::resolve_action_credentials dep|rustfs/src/config/info.rs|infra->app|crate::app::context::resolve_buffer_config +dep|rustfs/src/init.rs|infra->app|crate::app::context::resolve_notify_interface dep|rustfs/src/init.rs|infra->app|crate::app::context::resolve_region dep|rustfs/src/init.rs|infra->interface|crate::admin dep|rustfs/src/protocols/client.rs|infra->app|crate::app::context::resolve_action_credentials dep|rustfs/src/protocols/client.rs|infra->interface|crate::storage::ecfs::FS dep|rustfs/src/server/audit.rs|infra->app|crate::app::context::resolve_server_config +dep|rustfs/src/server/event.rs|infra->app|crate::app::context::resolve_notify_interface dep|rustfs/src/server/event.rs|infra->app|crate::app::context::resolve_server_config dep|rustfs/src/server/http.rs|infra->interface|crate::admin dep|rustfs/src/server/layer.rs|infra->app|crate::app::context::resolve_kms_runtime_service_manager @@ -69,6 +71,7 @@ dep|rustfs/src/storage/access.rs|infra->app|crate::app::context::resolve_region dep|rustfs/src/storage/concurrency/manager.rs|infra->app|crate::app::context::resolve_performance_metrics dep|rustfs/src/storage/sse.rs|infra->app|crate::app::context::resolve_encryption_service dep|rustfs/src/storage/helper.rs|infra->app|crate::app::context::resolve_action_credentials +dep|rustfs/src/storage/helper.rs|infra->app|crate::app::context::resolve_notify_interface dep|rustfs/src/storage/rpc/disk.rs|infra->app|crate::app::context::resolve_internode_metrics dep|rustfs/src/storage/rpc/health.rs|infra->app|crate::app::context::resolve_local_node_name dep|rustfs/src/storage/rpc/http_service.rs|infra->app|crate::app::context::resolve_internode_metrics