refactor(admin): migrate OIDC consumers to app context (#3800)

This commit is contained in:
houseme
2026-06-24 12:51:53 +08:00
committed by GitHub
parent 89dfb1357c
commit cbf526df87
13 changed files with 88 additions and 31 deletions
+1
View File
@@ -136,6 +136,7 @@ impl Config {
let http_prefix = rustfs_config::RUSTFS_HTTP_PREFIX;
// Collect OIDC provider info if available
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
let oidc = resolve_oidc_handle()
.map(|sys| {
sys.list_visible_providers()
+5
View File
@@ -268,6 +268,7 @@ pub struct ListOidcProvidersHandler {}
#[async_trait::async_trait]
impl Operation for ListOidcProvidersHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
let oidc_sys = resolve_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
let providers = oidc_sys.list_visible_providers();
@@ -439,6 +440,7 @@ impl Operation for OidcAuthorizeHandler {
return Err(s3_error!(InvalidRequest, "invalid provider_id"));
}
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
let oidc_sys = resolve_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
// Derive the callback redirect URI from the request
@@ -512,6 +514,7 @@ impl Operation for OidcCallbackHandler {
));
}
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
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,6 +602,7 @@ impl Operation for OidcLogoutHandler {
return redirect_response(&fallback_location);
};
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
let location = match resolve_oidc_handle() {
Some(oidc_sys) => match oidc_sys.build_logout_url(&logout_token, &fallback_location).await {
Ok(Some(url)) => url,
@@ -628,6 +632,7 @@ impl Operation for OidcLogoutHandler {
/// an explicit redirect_uri is recommended to prevent header manipulation.
fn derive_callback_uri(req: &S3Request<Body>, provider_id: &str) -> S3Result<String> {
// Use explicitly configured redirect_uri if available
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
if let Some(oidc_sys) = resolve_oidc_handle()
&& let Some(config) = oidc_sys.get_provider_config(provider_id)
{
+2
View File
@@ -59,6 +59,7 @@ fn has_identity_authorization_context(policies: &[String], groups: &[String]) ->
}
fn configured_roles_claim_key(provider_id: &str) -> Option<String> {
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
resolve_oidc_handle()
.as_ref()
.and_then(|oidc_sys| oidc_sys.get_provider_config(provider_id))
@@ -316,6 +317,7 @@ async fn handle_assume_role_with_web_identity(body: AssumeRoleRequest) -> S3Resu
}
// Verify the JWT and extract claims
// RUSTFS_COMPAT_TODO(CTX-002): admin OIDC consumers still depend on the resolver's global fallback while AppContext OIDC wiring is incomplete. Remove after OIDC ownership moves fully into AppContext and the global fallback is retired.
let oidc_sys = resolve_oidc_handle().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?;
let (claims, provider_id) = oidc_sys
+20 -9
View File
@@ -15,7 +15,8 @@
use super::super::storageclass;
use super::super::{STORAGE_CLASS_SUB_SYS, read_admin_config_without_migrate};
use crate::app::context::{
publish_server_config, publish_storage_class_config, resolve_notification_system, resolve_object_store_handle,
AppContext, get_global_app_context, publish_server_config, publish_storage_class_config, resolve_notification_system,
resolve_object_store_handle, resolve_object_store_handle_for_context,
};
use rustfs_audit::reload_audit_config;
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_REDIS_DEFAULT_CHANNEL, AUDIT_WEBHOOK_SUB_SYS};
@@ -66,6 +67,10 @@ fn invalid_request(message: impl Into<String>) -> S3Error {
S3Error::with_message(S3ErrorCode::InvalidRequest, message.into())
}
fn resolve_runtime_config_store_for_context(context: Option<&AppContext>) -> S3Result<std::sync::Arc<crate::app::ECStore>> {
resolve_object_store_handle_for_context(context).ok_or_else(|| internal_error("storage layer not initialized"))
}
async fn apply_storage_class_runtime_config(config: &ServerConfig) -> S3Result<()> {
let Some(store) = resolve_object_store_handle() else {
return Err(internal_error("storage layer not initialized"));
@@ -292,14 +297,12 @@ pub async fn apply_dynamic_config_for_subsystem(config: &ServerConfig, sub_syste
Ok(true)
}
pub async fn reload_dynamic_config_runtime_state(sub_system: &str) -> S3Result<()> {
pub async fn reload_dynamic_config_runtime_state_for_context(context: Option<&AppContext>, sub_system: &str) -> S3Result<()> {
if !is_dynamic_config_subsystem(sub_system) {
return Err(internal_error(format!("unsupported dynamic config subsystem: {sub_system}")));
}
let Some(store) = resolve_object_store_handle() else {
return Err(internal_error("storage layer not initialized"));
};
let store = resolve_runtime_config_store_for_context(context)?;
let config = read_admin_config_without_migrate(store).await.map_err(|err| {
warn!("peer reload_dynamic_config: failed to load server config for {sub_system}: {err}");
@@ -312,10 +315,13 @@ pub async fn reload_dynamic_config_runtime_state(sub_system: &str) -> S3Result<(
Ok(())
}
pub async fn reload_runtime_config_snapshot() -> S3Result<()> {
let Some(store) = resolve_object_store_handle() else {
return Err(internal_error("storage layer not initialized"));
};
pub async fn reload_dynamic_config_runtime_state(sub_system: &str) -> S3Result<()> {
let context = get_global_app_context();
reload_dynamic_config_runtime_state_for_context(context.as_deref(), sub_system).await
}
pub async fn reload_runtime_config_snapshot_for_context(context: Option<&AppContext>) -> S3Result<()> {
let store = resolve_runtime_config_store_for_context(context)?;
let config = read_admin_config_without_migrate(store).await.map_err(|err| {
warn!("peer reload_runtime_config_snapshot: failed to load server config: {err}");
@@ -340,6 +346,11 @@ pub async fn reload_runtime_config_snapshot() -> S3Result<()> {
Ok(())
}
pub async fn reload_runtime_config_snapshot() -> S3Result<()> {
let context = get_global_app_context();
reload_runtime_config_snapshot_for_context(context.as_deref()).await
}
pub async fn signal_dynamic_config_reload(sub_system: &str) {
if !is_dynamic_config_subsystem(sub_system) {
return;
+8 -3
View File
@@ -15,7 +15,7 @@
use super::super::Error as StorageError;
use super::super::{read_admin_config, save_admin_config};
use crate::admin::site_replication_identity::{deployment_id_for_endpoint, normalize_peer_map_by_identity_with};
use crate::app::context::resolve_object_store_handle;
use crate::app::context::{AppContext, get_global_app_context, resolve_object_store_handle_for_context};
use rustfs_madmin::PeerInfo;
use s3s::{S3Error, S3ErrorCode, S3Result};
use serde_json::{Map, Value};
@@ -90,8 +90,8 @@ fn normalize_site_replication_state_json(data: &[u8]) -> Result<Option<Vec<u8>>,
///
/// RustFS does not currently keep a separate in-memory cache for this state,
/// so "reload" means validating that the persisted JSON is readable.
pub async fn reload_site_replication_runtime_state() -> S3Result<()> {
let Some(store) = resolve_object_store_handle() else {
pub async fn reload_site_replication_runtime_state_for_context(context: Option<&AppContext>) -> S3Result<()> {
let Some(store) = resolve_object_store_handle_for_context(context) else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
@@ -116,6 +116,11 @@ pub async fn reload_site_replication_runtime_state() -> S3Result<()> {
}
}
pub async fn reload_site_replication_runtime_state() -> S3Result<()> {
let context = get_global_app_context();
reload_site_replication_runtime_state_for_context(context.as_deref()).await
}
#[cfg(test)]
mod tests {
use super::*;
+7 -5
View File
@@ -84,16 +84,16 @@ pub fn resolve_iam_handle() -> Option<Arc<IamSys<ObjectStore>>> {
resolve_iam_handle_with(get_global_app_context(), rustfs_iam::get_global_iam_sys)
}
/// Resolve a ready IAM system handle using AppContext-first precedence.
pub fn resolve_ready_iam_handle() -> rustfs_iam::error::Result<Arc<IamSys<ObjectStore>>> {
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<Arc<OidcSys>> {
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<Arc<IamSys<ObjectStore>>> {
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<String> {
resolve_token_signing_key_with(get_global_app_context(), rustfs_iam::manager::get_token_signing_key)
@@ -1254,6 +1254,8 @@ mod tests {
assert_eq!(resolve_outbound_tls_generation_with(None, || TlsGeneration(99)), TlsGeneration(99));
assert!(!resolve_iam_ready_with(None, || false));
assert!(resolve_iam_handle_with(None, || None).is_none());
// OIDC fallback path: both context absent and fallback return None.
assert!(resolve_oidc_handle_with(None, || None).is_none());
assert!(Arc::ptr_eq(
&resolve_bucket_metadata_handle_with(None, || Some(bucket_metadata.clone())).expect("fallback bucket metadata"),
&bucket_metadata
+2 -1
View File
@@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::storage::{Error as StorageError, read_config, resolve_object_store_handle, save_config};
use crate::app::context::resolve_object_store_handle;
use crate::storage::{Error as StorageError, read_config, save_config};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
+4 -2
View File
@@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::app::context::{resolve_endpoints_handle, resolve_iam_ready, resolve_lock_clients_handle};
use crate::app::context::{
resolve_endpoints_handle, resolve_iam_ready, resolve_lock_clients_handle, resolve_object_store_handle,
};
use crate::server::{ServiceState, ServiceStateManager};
use crate::server::{has_path_prefix, is_table_catalog_path};
use crate::storage::{Endpoint, EndpointServerPools, is_dist_erasure, resolve_object_store_handle};
use crate::storage::{Endpoint, EndpointServerPools, is_dist_erasure};
#[cfg(test)]
use crate::storage::{Endpoints, PoolEndpoints};
use bytes::Bytes;
+1 -1
View File
@@ -42,7 +42,7 @@ impl NodeService {
}));
}
let Some(store) = resolve_object_store_handle() else {
let Some(store) = self.resolve_object_store() else {
return Ok(Response::new(LoadBucketMetadataResponse {
success: false,
error_info: Some("errServerNotInitialized".to_string()),
+1 -1
View File
@@ -215,7 +215,7 @@ impl NodeService {
&self,
_request: Request<LocalStorageInfoRequest>,
) -> Result<Response<LocalStorageInfoResponse>, Status> {
let Some(store) = resolve_object_store_handle() else {
let Some(store) = self.resolve_object_store() else {
return Ok(Response::new(LocalStorageInfoResponse {
success: false,
storage_info: Bytes::new(),
+29 -8
View File
@@ -23,7 +23,9 @@ use crate::admin::service::{
config::{reload_dynamic_config_runtime_state, reload_runtime_config_snapshot},
site_replication::reload_site_replication_runtime_state,
};
use crate::app::context::{resolve_iam_handle, resolve_lock_client};
use crate::app::context::{
AppContext, get_global_app_context, resolve_iam_handle, resolve_lock_client, resolve_object_store_handle_for_context,
};
use bytes::Bytes;
use futures::Stream;
use futures_util::future::join_all;
@@ -171,17 +173,36 @@ mod lock;
#[path = "metrics.rs"]
mod metrics;
#[derive(Debug)]
pub struct NodeService {
local_peer: LocalPeerS3Client,
context: Option<Arc<AppContext>>,
}
impl std::fmt::Debug for NodeService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeService")
.field("local_peer", &self.local_peer)
.field("context_present", &self.context.is_some())
.finish()
}
}
pub fn make_server() -> NodeService {
let context = get_global_app_context();
make_server_for_context(context)
}
pub fn make_server_for_context(context: Option<Arc<AppContext>>) -> NodeService {
let local_peer = LocalPeerS3Client::new(None, None);
NodeService { local_peer }
NodeService { local_peer, context }
}
impl NodeService {
fn resolve_object_store(&self) -> Option<Arc<crate::app::ECStore>> {
let context = self.context.clone().or_else(get_global_app_context);
resolve_object_store_handle_for_context(context.as_deref())
}
async fn find_disk(&self, disk_path: &str) -> Option<DiskStore> {
find_local_disk_by_ref(disk_path).await
}
@@ -900,7 +921,7 @@ impl Node for NodeService {
&self,
_request: Request<ReloadSiteReplicationConfigRequest>,
) -> Result<Response<ReloadSiteReplicationConfigResponse>, Status> {
let Some(_store) = resolve_object_store_handle() else {
let Some(_store) = self.resolve_object_store() else {
return Ok(Response::new(ReloadSiteReplicationConfigResponse {
success: false,
error_info: Some("errServerNotInitialized".to_string()),
@@ -989,7 +1010,7 @@ impl Node for NodeService {
&self,
_request: Request<ReloadPoolMetaRequest>,
) -> Result<Response<ReloadPoolMetaResponse>, Status> {
let Some(store) = resolve_object_store_handle() else {
let Some(store) = self.resolve_object_store() else {
return Ok(Response::new(ReloadPoolMetaResponse {
success: false,
error_info: Some("errServerNotInitialized".to_string()),
@@ -1014,7 +1035,7 @@ impl Node for NodeService {
}
async fn stop_rebalance(&self, request: Request<StopRebalanceRequest>) -> Result<Response<StopRebalanceResponse>, Status> {
let Some(store) = resolve_object_store_handle() else {
let Some(store) = self.resolve_object_store() else {
return Ok(Response::new(StopRebalanceResponse {
success: false,
error_info: Some("errServerNotInitialized".to_string()),
@@ -1035,7 +1056,7 @@ impl Node for NodeService {
request: Request<LoadRebalanceMetaRequest>,
) -> Result<Response<LoadRebalanceMetaResponse>, Status> {
let LoadRebalanceMetaRequest { start_rebalance } = request.into_inner();
let Some(store) = resolve_object_store_handle() else {
let Some(store) = self.resolve_object_store() else {
log_load_rebalance_meta_rejected!("server_not_initialized", start_rebalance);
return Ok(Response::new(LoadRebalanceMetaResponse {
success: false,
@@ -1174,7 +1195,7 @@ impl Node for NodeService {
&self,
_request: Request<LoadTransitionTierConfigRequest>,
) -> Result<Response<LoadTransitionTierConfigResponse>, Status> {
let Some(store) = resolve_object_store_handle() else {
let Some(store) = self.resolve_object_store() else {
return Ok(Response::new(LoadTransitionTierConfigResponse {
success: false,
error_info: Some("errServerNotInitialized".to_string()),