diff --git a/rustfs/src/admin/auth.rs b/rustfs/src/admin/auth.rs index 41bb6155b..edcc03101 100644 --- a/rustfs/src/admin/auth.rs +++ b/rustfs/src/admin/auth.rs @@ -13,13 +13,14 @@ // limitations under the License. use crate::auth::get_condition_values; +use crate::server::RemoteAddr; use http::HeaderMap; use http::Uri; use rustfs_credentials::Credentials; use rustfs_iam::store::Store; use rustfs_iam::sys::IamSys; use rustfs_policy::policy::{Args, action::Action}; -use s3s::{S3Result, s3_error}; +use s3s::{Body, S3Request, S3Result, s3_error}; use std::sync::Arc; use tracing::debug; @@ -292,6 +293,25 @@ pub async fn authenticate_request( result } +/// Full admin gate over an `S3Request`: extract the request credentials, +/// authenticate them ([`authenticate_request`]), then authorize the caller for +/// `actions` ([`validate_admin_request`], allowing on the first permitted +/// action). Returns the authenticated credentials for handlers that need the +/// caller identity. `deny_only` stays `false`: every caller performs a full +/// allow check. +pub async fn authorize_admin_request(req: &S3Request, actions: Vec) -> S3Result { + let Some(input_cred) = req.credentials.as_ref() else { + return Err(s3_error!(InvalidRequest, "get cred failed")); + }; + + let (cred, owner) = authenticate_request(&req.headers, &req.uri, input_cred).await?; + + let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + validate_admin_request(&req.headers, &cred, owner, false, actions, remote_addr).await?; + + Ok(cred) +} + #[cfg(test)] mod tests { //! Unit coverage for the central admin authorization gate (rustfs/backlog#1151 sec-4). @@ -570,6 +590,30 @@ mod tests { assert_access_denied(res); } + /// The shared admin gate rejects a request carrying no credentials before + /// authentication or IAM is consulted, with the exact error the folded + /// per-handler wrappers produced (rustfs/backlog#1829). + #[tokio::test] + async fn authorize_admin_request_without_credentials_is_rejected() { + let req = S3Request { + input: Body::from(String::new()), + method: http::Method::GET, + uri: Uri::from_static("/rustfs/admin/v3/list-jobs"), + headers: HeaderMap::new(), + extensions: http::Extensions::new(), + credentials: None, + region: None, + service: None, + trailing_headers: None, + }; + + let err = authorize_admin_request(&req, vec![admin_action()]) + .await + .expect_err("a request without credentials must be rejected"); + assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest); + assert_eq!(err.message(), Some("get cred failed")); + } + /// KMS scoping rides the object slot with an empty bucket, matching the /// contract the policy crate evaluates KMS statements against. #[test] diff --git a/rustfs/src/admin/handlers/batch_job.rs b/rustfs/src/admin/handlers/batch_job.rs index 707f98d28..ca3f0e07d 100644 --- a/rustfs/src/admin/handlers/batch_job.rs +++ b/rustfs/src/admin/handlers/batch_job.rs @@ -35,11 +35,10 @@ //! When RustFS grows a real batch-job engine, these handlers should be rewired to //! it; the request parsing and response shapes here are intended to stay stable. -use crate::admin::auth::validate_admin_request; +use crate::admin::auth::authorize_admin_request; use crate::admin::router::{AdminOperation, Operation, S3Router}; use crate::admin::utils::read_compatible_admin_body; -use crate::auth::{check_key_valid, get_session_token}; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use http::{HeaderMap, HeaderValue, Uri}; use hyper::{Method, StatusCode}; use matchit::Params; @@ -70,17 +69,7 @@ fn extract_query_params(uri: &Uri) -> HashMap { } async fn validate_batch_job_admin_request(req: &S3Request, action: AdminAction) -> S3Result { - let Some(input_cred) = req.credentials.as_ref() else { - return Err(s3_error!(InvalidRequest, "get cred failed")); - }; - - let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; - - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); - validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await?; - - Ok(cred) + authorize_admin_request(req, vec![Action::AdminAction(action)]).await } fn json_response(status: StatusCode, value: &T) -> S3Result> { diff --git a/rustfs/src/admin/handlers/config_admin.rs b/rustfs/src/admin/handlers/config_admin.rs index 83641b017..82096b741 100644 --- a/rustfs/src/admin/handlers/config_admin.rs +++ b/rustfs/src/admin/handlers/config_admin.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::auth::validate_admin_request; +use crate::admin::auth::authorize_admin_request; use crate::admin::handlers::supervise_admin_mutation; use crate::admin::router::{AdminOperation, Operation, S3Router}; use crate::admin::runtime_sources::{ @@ -31,9 +31,8 @@ use crate::admin::storage_api::config::{ }; use crate::admin::storage_api::contract::list::ListOperations as _; use crate::admin::utils::{encode_compatible_admin_payload, is_compat_admin_request, read_compatible_admin_body}; -use crate::auth::{check_key_valid, get_session_token}; use crate::error::ApiError; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use http::{HeaderMap, HeaderValue, Uri}; use hyper::{Method, StatusCode}; use matchit::Params; @@ -678,28 +677,12 @@ fn extract_query_params(uri: &Uri) -> HashMap { } async fn validate_config_admin_request(req: &S3Request) -> S3Result { - let Some(input_cred) = req.credentials.as_ref() else { + // Pre-check keeps this endpoint's historical missing-credentials message; + // the shared gate reports "get cred failed". + if req.credentials.is_none() { return Err(s3_error!(InvalidRequest, "missing credentials")); - }; - - let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; - - let remote_addr = req - .extensions - .get::>() - .and_then(|opt| opt.map(|addr| addr.0)); - validate_admin_request( - &req.headers, - &cred, - owner, - false, - vec![Action::AdminAction(AdminAction::ConfigUpdateAdminAction)], - remote_addr, - ) - .await?; - - Ok(cred) + } + authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ConfigUpdateAdminAction)]).await } fn header_value(content_type: &str) -> S3Result { @@ -2302,6 +2285,30 @@ mod tests { use serial_test::serial; use temp_env::with_vars; + /// The config-admin gate historically reports "missing credentials" (not the + /// shared gate's "get cred failed"); the pre-check in + /// `validate_config_admin_request` must keep that message byte-identical. + #[tokio::test] + async fn config_admin_request_without_credentials_keeps_historical_message() { + let req = S3Request { + input: Body::from(String::new()), + method: Method::GET, + uri: Uri::from_static("/rustfs/admin/v3/config"), + headers: HeaderMap::new(), + extensions: http::Extensions::new(), + credentials: None, + region: None, + service: None, + trailing_headers: None, + }; + + let err = validate_config_admin_request(&req) + .await + .expect_err("a request without credentials must be rejected"); + assert_eq!(err.code(), &S3ErrorCode::InvalidRequest); + assert_eq!(err.message(), Some("missing credentials")); + } + #[test] fn config_preflight_covers_each_runtime_worker_family() { assert_eq!(config_preflight_subsystems(Some(SCANNER_SUB_SYS)), [SCANNER_SUB_SYS]); diff --git a/rustfs/src/admin/handlers/replication.rs b/rustfs/src/admin/handlers/replication.rs index 196c0df3b..0d0c1e8ce 100644 --- a/rustfs/src/admin/handlers/replication.rs +++ b/rustfs/src/admin/handlers/replication.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::auth::validate_admin_request; +use crate::admin::auth::authorize_admin_request; use crate::admin::handlers::site_replication::site_replication_peer_deployment_id_for_endpoint; use crate::admin::router::{AdminOperation, Operation, S3Router}; use crate::admin::runtime_sources::{ @@ -35,9 +35,8 @@ use crate::admin::storage_api::contract::list::ListOperations as _; use crate::admin::storage_api::error::StorageError; use crate::admin::storage_api::runtime::PeerRestClient; use crate::admin::utils::read_compatible_admin_body; -use crate::auth::{check_key_valid, get_session_token}; use crate::error::ApiError; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use crate::storage::storage_api::lock_bucket_targets_metadata; use http::{HeaderMap, HeaderValue, Uri}; use hyper::{Method, StatusCode}; @@ -454,17 +453,7 @@ pub fn register_replication_route(r: &mut S3Router) -> std::io:: } async fn validate_replication_admin_request(req: &S3Request, action: AdminAction) -> S3Result { - let Some(input_cred) = req.credentials.as_ref() else { - return Err(s3_error!(InvalidRequest, "get cred failed")); - }; - - let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; - - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); - validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await?; - - Ok(cred) + authorize_admin_request(req, vec![Action::AdminAction(action)]).await } #[allow(dead_code)] diff --git a/rustfs/src/admin/handlers/site_replication.rs b/rustfs/src/admin/handlers/site_replication.rs index c07c85ad1..85d529836 100644 --- a/rustfs/src/admin/handlers/site_replication.rs +++ b/rustfs/src/admin/handlers/site_replication.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::auth::validate_admin_request; +use crate::admin::auth::authorize_admin_request; use crate::admin::router::{AdminOperation, Operation, S3Router}; use crate::admin::runtime_sources::{ current_deployment_id, current_endpoints_handle, current_federated_identity_service, current_iam_handle, @@ -41,10 +41,10 @@ use crate::admin::storage_api::contract::bucket::{ use crate::admin::storage_api::error::Error as StorageError; use crate::admin::storage_api::runtime::ECStore; use crate::admin::utils::{encode_compatible_admin_payload, read_compatible_admin_body}; -use crate::auth::{check_key_valid, constant_time_eq, get_session_token}; +use crate::auth::constant_time_eq; use crate::config::get_config_snapshot; use crate::error::ApiError; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use crate::storage::storage_api::{ delete_config_no_lock, lock_bucket_targets_metadata, read_config_no_lock, save_config_no_lock, with_config_object_read_lock, with_config_object_write_lock, @@ -916,17 +916,7 @@ async fn validate_site_replication_admin_request( req: &S3Request, action: AdminAction, ) -> S3Result { - let Some(input_cred) = req.credentials.as_ref() else { - return Err(s3_error!(InvalidRequest, "get cred failed")); - }; - - let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; - - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); - validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await?; - - Ok(cred) + authorize_admin_request(req, vec![Action::AdminAction(action)]).await } fn reject_site_replicator_on_public_admin(cred: &rustfs_credentials::Credentials) -> S3Result<()> {