From b783f404ad4acaaddcd65461ae1c86a0e2f853bb Mon Sep 17 00:00:00 2001 From: Chris An Date: Mon, 26 Oct 2026 22:23:47 +0800 Subject: [PATCH] refactor(admin): migrate identity and cluster handlers to owned requests (#8966) * refactor(admin): migrate identity and cluster handlers to owned requests Move the T1.12 batch 4 admin handlers (user, service_account, oidc, tier, system, pools, quota, kms_dynamic, kms_backup, rebalance, module_switch, bucket_meta, cluster_snapshot, realtime, bucket_recovery, profile) from the s3s request/response types to the owned AdminRequest/AdminResponse boundary. All 90 impl Operation blocks now take AdminRequest and return AdminResponse; handler logic, authorization order, error codes and response bytes are unchanged. The handlers use the shared owned helpers (owned_json_response, owned_admin_json_response, authorize_profile_envelope, the envelope-form runtime sources and audit constructors) instead of local copies. profile.rs drops the s3s forms of authorize_profile_request and profile_not_implemented_response now that profile_admin.rs is on the envelope gate too; profile_not_implemented_response answers with an AdminResponse. The realtime metrics stream moves from the s3s ByteStream adapter to Body::from_stream, and the rebalance idle-404 document still renders through s3s::S3Error::to_http_response with its body converted at the edge. The idp_compat delegators ImportIamV2 and AddServiceAccountLdap move to the owned trait as well: with their targets on both traits, the method-call delegation became ambiguous between the owned impl and the router blanket impl, so they now take the owned request directly. KmsAdminAudit::from_request loses its last production caller with this batch and is deleted; its comparison test now asserts the envelope facts directly. Lower the s3s footprint baseline to the measured 68 files. Part of rustfs/backlog#2788 (rustfs/backlog#2734 T1.12). * refactor(admin): remove the legacy admin helpers batch 4 superseded Batch 4 moved the last production callers of the s3s-typed admin helpers to their owned forms, which left `admin_json_response`, `json_response`, `authenticate_request`, `authorize_admin_request`, `object_store_from_req` and `object_store_from_extensions` unused, along with the `Body`, `S3Request` and `S3Response` re-exports of the admin `s3` facade. Remove them. The owned forms keep their documentation; the behavior tests of the removed `json_response` now exercise `owned_json_response`, and the compat-prefix test asserts the owned response directly. Two tests existed only to compare a removed legacy helper with its owned twin (`owned_json_response_matches_the_legacy_shape`) or to cover the removed legacy gate (`authorize_admin_request_without_credentials_is_rejected`); they go with the helpers, and the owned equivalents stay. The rustfs test-count row drops from 5957 to 5955 and the s3s footprint from 68 to 66 files. --- .config/s3s-footprint-baseline.txt | 2 +- .config/test-count-baseline.txt | 2 +- rustfs/src/admin/auth.rs | 82 +------- rustfs/src/admin/handlers/bucket_meta.rs | 135 +++++-------- rustfs/src/admin/handlers/bucket_recovery.rs | 24 +-- rustfs/src/admin/handlers/cluster_snapshot.rs | 45 ++--- rustfs/src/admin/handlers/idp_compat.rs | 22 +- rustfs/src/admin/handlers/kms_audit.rs | 42 +--- rustfs/src/admin/handlers/kms_backup.rs | 53 +++-- rustfs/src/admin/handlers/kms_dynamic.rs | 178 ++++++----------- rustfs/src/admin/handlers/mod.rs | 49 ++--- rustfs/src/admin/handlers/module_switch.rs | 55 +++-- rustfs/src/admin/handlers/oidc.rs | 151 +++++++------- rustfs/src/admin/handlers/pools.rs | 138 ++++++------- rustfs/src/admin/handlers/profile.rs | 81 ++------ rustfs/src/admin/handlers/quota.rs | 99 +++++---- rustfs/src/admin/handlers/realtime.rs | 72 ++----- rustfs/src/admin/handlers/rebalance.rs | 101 +++++----- rustfs/src/admin/handlers/service_account.rs | 189 +++++++----------- rustfs/src/admin/handlers/system.rs | 148 ++++++-------- rustfs/src/admin/handlers/tier.rs | 120 ++++++----- rustfs/src/admin/handlers/user.rs | 128 ++++++------ rustfs/src/admin/runtime_sources.rs | 30 +-- rustfs/src/admin/storage_api.rs | 2 +- rustfs/src/admin/utils.rs | 59 ++---- 25 files changed, 779 insertions(+), 1228 deletions(-) diff --git a/.config/s3s-footprint-baseline.txt b/.config/s3s-footprint-baseline.txt index 58a6b3c2a..a42740b21 100644 --- a/.config/s3s-footprint-baseline.txt +++ b/.config/s3s-footprint-baseline.txt @@ -12,6 +12,6 @@ # s3_error_lines lines holding s3_error!, same scope, minus those the script # header's rule clears (rustfs_s3_types's macro); fails closed # ecstore_files Rust files referencing s3s paths under crates/ecstore/src -files=82 +files=66 s3_error_lines=289 ecstore_files=0 diff --git a/.config/test-count-baseline.txt b/.config/test-count-baseline.txt index e5168e766..96c9fe51e 100644 --- a/.config/test-count-baseline.txt +++ b/.config/test-count-baseline.txt @@ -10,7 +10,7 @@ # intentionally removed. Regenerate with: # scripts/check_test_count_ratchet.sh --update e2e_test 956 20 -rustfs 5957 64 +rustfs 5955 64 rustfs-audit 70 0 rustfs-checksums 26 0 rustfs-common 36 0 diff --git a/rustfs/src/admin/auth.rs b/rustfs/src/admin/auth.rs index 3fced06e2..a182407c5 100644 --- a/rustfs/src/admin/auth.rs +++ b/rustfs/src/admin/auth.rs @@ -14,15 +14,12 @@ use crate::app::{RequestCredentials, RequestEnvelope}; 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 rustfs_s3_types::{S3Result, s3_error}; -use s3s::{Body, S3Request}; use std::sync::Arc; use tracing::debug; @@ -238,37 +235,6 @@ pub async fn validate_admin_request_with_kms_key( .await } -/// Unified authentication request handler for both UI and CLI -/// -/// This function provides a single entry point for authentication, -/// ensuring consistent behavior between UI and CLI authentication flows. -/// -/// # Arguments -/// * `headers` - HTTP request headers -/// * `uri` - Request URI -/// * `credentials` - User credentials from request (Credentials) -/// -/// # Returns -/// * `Ok((Credentials, bool))` - Authentication successful, returns user credentials and is_owner flag -/// * `Err(S3Error)` - Authentication failed with error details -/// -/// # Example -/// ```ignore -/// let (cred, is_owner) = authenticate_request(&req.headers, &req.uri, &input_cred).await?; -/// ``` -pub async fn authenticate_request( - headers: &HeaderMap, - uri: &Uri, - credentials: &s3s::auth::Credentials, -) -> S3Result<(Credentials, bool)> { - use crate::auth::get_session_token; - - // Extract session token from request - let session_token = get_session_token(uri, headers).unwrap_or_default(); - - authenticate_request_facts(&credentials.access_key, session_token).await -} - /// Authenticate the caller of an owned admin request from the facts its /// envelope carries: the presented access key and the session token the edge /// decoded. @@ -304,26 +270,12 @@ async fn authenticate_request_facts(access_key: &str, session_token: &str) -> S3 result.map_err(Into::into) } -/// 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) -} - -/// Full admin gate over RustFS-owned request facts. +/// Full admin gate over RustFS-owned request facts: authenticate the +/// presented credentials, 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_envelope(req: &RequestEnvelope, actions: Vec) -> S3Result { let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "get cred failed")); @@ -348,6 +300,7 @@ mod tests { //! empty in-memory cache so `is_owner=true` returns allow (owner short-circuit) //! and every non-owner principal resolves to deny. use super::*; + use http::Uri; use rustfs_iam::cache::Cache; use rustfs_iam::manager::IamCache; use rustfs_iam::store::{GroupInfo, MappedPolicy, UserType}; @@ -616,27 +569,6 @@ mod tests { /// 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(), &rustfs_s3_types::S3ErrorCode::InvalidRequest); - assert_eq!(err.message(), Some("get cred failed")); - } - #[tokio::test] async fn authorize_admin_envelope_without_credentials_is_rejected() { let envelope = crate::app::RequestEnvelope::from_parts( diff --git a/rustfs/src/admin/handlers/bucket_meta.rs b/rustfs/src/admin/handlers/bucket_meta.rs index 2358a6529..b622282e1 100644 --- a/rustfs/src/admin/handlers/bucket_meta.rs +++ b/rustfs/src/admin/handlers/bucket_meta.rs @@ -13,7 +13,8 @@ // limitations under the License. use crate::admin::handlers::site_replication::site_replication_bucket_meta_hook; -use crate::admin::runtime_sources::object_store_from_extensions; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::runtime_sources::object_store_from_envelope; use crate::admin::storage_api::bucket::utils::{deserialize, serialize}; use crate::admin::storage_api::bucket::{ metadata::{ @@ -30,11 +31,12 @@ use crate::admin::storage_api::error::StorageError; use crate::storage::storage_api::lock_bucket_targets_metadata; use crate::{ admin::{ - auth::authorize_admin_request, - router::{AdminOperation, Operation, S3Router}, + auth::authorize_admin_envelope, + router::{AdminOperation, S3Router}, }, server::ADMIN_PREFIX, }; +use http::header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE}; use http::{HeaderMap, StatusCode}; use hyper::Method; use matchit::Params; @@ -50,12 +52,8 @@ use rustfs_policy::policy::{ BucketPolicy, action::{Action, AdminAction}, }; -use rustfs_s3_types::{S3Result, s3_error}; +use rustfs_s3_types::{Body, S3Result, s3_error}; use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf}; -use s3s::{ - Body, S3Request, S3Response, - header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE}, -}; use serde::Deserialize; use serde_urlencoded::from_bytes; use std::{ @@ -357,9 +355,9 @@ pub fn register_bucket_meta_route(r: &mut S3Router) -> std::io:: #[async_trait::async_trait] impl Operation for ExportBucketMetadata { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: ExportBucketMetadataQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -368,13 +366,13 @@ impl Operation for ExportBucketMetadata { } }; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ExportBucketMetadataAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ExportBucketMetadataAction)]).await?; - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object store is not initialized")); }; @@ -477,7 +475,7 @@ impl Operation for ExportBucketMetadata { header.insert("x-rustfs-bucket-metadata-export", "diagnostic".parse().expect("valid header value")); } header.insert(CONTENT_LENGTH, zip_bytes.get_ref().len().to_string().parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(zip_bytes.into_inner())), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(zip_bytes.into_inner()), header)) } } @@ -490,9 +488,9 @@ pub struct ImportBucketMetadata {} #[async_trait::async_trait] impl Operation for ImportBucketMetadata { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let _query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: ImportBucketMetadataQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -501,13 +499,13 @@ impl Operation for ImportBucketMetadata { } }; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ImportBucketMetadataAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ImportBucketMetadataAction)]).await?; - let mut input = req.input; + let mut input = req.take_body(); let body = match input.store_all_limited(MAX_BUCKET_METADATA_IMPORT_SIZE).await { Ok(b) => b, Err(e) => { @@ -629,7 +627,7 @@ impl Operation for ImportBucketMetadata { }; } - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object store is not initialized")); }; @@ -816,7 +814,7 @@ impl Operation for ImportBucketMetadata { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } @@ -1457,21 +1455,13 @@ mod backup_zip_compatibility_tests { writer.finish().expect("finish compatibility archive").into_inner() } - fn admin_request(method: Method, uri: Uri, body: Vec) -> S3Request { - S3Request { - input: Body::from(body), - method, - uri, - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: Some(s3s::auth::Credentials { - access_key: ROOT_ACCESS_KEY.to_string(), - secret_key: s3s::auth::SecretKey::from(ROOT_SECRET_KEY.to_string()), - }), - region: None, - service: None, - trailing_headers: None, - } + fn admin_request(method: Method, uri: Uri, body: Vec) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, uri) + .headers(HeaderMap::new()) + .extensions(Extensions::new()) + .body(Body::from(body)) + .credentials(ROOT_ACCESS_KEY, ROOT_SECRET_KEY, None) + .build() } async fn import_archive(archive: Vec) { @@ -1482,7 +1472,8 @@ mod backup_zip_compatibility_tests { ) .await .expect("root admin must import the compatibility archive"); - assert_eq!(response.output.0, StatusCode::OK); + let (status, _headers, _body) = response.into_parts(); + assert_eq!(status, StatusCode::OK); } async fn assert_unreadable_xml_export_is_explicit(config_file: &str) { @@ -1557,14 +1548,9 @@ mod backup_zip_compatibility_tests { ) .await .expect("diagnostic export must identify the omitted configuration"); - assert_eq!(diagnostic.output.0, StatusCode::OK); - let bytes = diagnostic - .output - .1 - .collect() - .await - .expect("read diagnostic archive") - .to_bytes(); + let (status, _headers, body) = diagnostic.into_parts(); + assert_eq!(status, StatusCode::OK); + let bytes = body.collect().await.expect("read diagnostic archive").to_bytes(); let mut archive = ZipArchive::new(Cursor::new(&bytes)).expect("open diagnostic archive"); let mut files = HashMap::new(); for index in 0..archive.len() { @@ -1698,10 +1684,11 @@ mod backup_zip_compatibility_tests { ) .await .expect("one unreadable bucket must not abort diagnostic export"); - assert_eq!(response.output.0, StatusCode::OK); - assert_eq!(response.headers["x-rustfs-bucket-metadata-export"], "diagnostic"); - assert_eq!(response.headers[CONTENT_DISPOSITION], "attachment; filename=bucket-meta-diagnostic.zip"); - let bytes = response.output.1.collect().await.expect("read diagnostic archive").to_bytes(); + let (status, headers, body) = response.into_parts(); + assert_eq!(status, StatusCode::OK); + assert_eq!(headers["x-rustfs-bucket-metadata-export"], "diagnostic"); + assert_eq!(headers[CONTENT_DISPOSITION], "attachment; filename=bucket-meta-diagnostic.zip"); + let bytes = body.collect().await.expect("read diagnostic archive").to_bytes(); let mut archive = ZipArchive::new(Cursor::new(&bytes)).expect("open diagnostic archive"); let mut files = HashMap::new(); for index in 0..archive.len() { @@ -1835,13 +1822,8 @@ mod backup_zip_compatibility_tests { ) .await .expect("diagnostic export after recovery"); - let bytes = response - .output - .1 - .collect() - .await - .expect("read complete diagnostic archive") - .to_bytes(); + let (_status, _headers, body) = response.into_parts(); + let bytes = body.collect().await.expect("read complete diagnostic archive").to_bytes(); let mut archive = ZipArchive::new(Cursor::new(&bytes)).expect("open complete diagnostic archive"); let manifest: serde_json::Value = serde_json::from_reader( archive @@ -1866,9 +1848,10 @@ mod backup_zip_compatibility_tests { ) .await .expect("ordinary cluster export must work after API recovery"); - assert!(!response.headers.contains_key("x-rustfs-bucket-metadata-export")); - assert_eq!(response.headers[CONTENT_DISPOSITION], "attachment; filename=bucket-meta.zip"); - let bytes = response.output.1.collect().await.expect("read ordinary archive").to_bytes(); + let (_status, headers, body) = response.into_parts(); + assert!(!headers.contains_key("x-rustfs-bucket-metadata-export")); + assert_eq!(headers[CONTENT_DISPOSITION], "attachment; filename=bucket-meta.zip"); + let bytes = body.collect().await.expect("read ordinary archive").to_bytes(); let mut archive = ZipArchive::new(Cursor::new(bytes)).expect("open ordinary archive"); assert!(archive.by_name(DIAGNOSTIC_EXPORT_MANIFEST).is_err()); assert!(archive.by_name(&format!("{HEALTHY}/{BUCKET_VERSIONING_CONFIG}")).is_ok()); @@ -1988,15 +1971,9 @@ mod backup_zip_compatibility_tests { ) .await .expect("root admin must export persisted bucket metadata"); - assert_eq!(export_response.output.0, StatusCode::OK); - let exported_archive = export_response - .output - .1 - .collect() - .await - .expect("read exported archive body") - .to_bytes() - .to_vec(); + let (status, _headers, body) = export_response.into_parts(); + assert_eq!(status, StatusCode::OK); + let exported_archive = body.collect().await.expect("read exported archive body").to_bytes().to_vec(); let mut archive = ZipArchive::new(Cursor::new(&exported_archive)).expect("open exported archive"); for (config_file, payload) in persisted_xml_fixtures() { let mut exported_payload = Vec::new(); @@ -2060,18 +2037,12 @@ mod shared_gate_tests { use http::Uri; use rustfs_s3_types::S3ErrorCode; - fn credential_less_request(method: Method, uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn credential_less_request(method: Method, uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(http::Extensions::new()) + .body(Body::empty()) + .build() } async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str) { @@ -2106,7 +2077,7 @@ mod shared_gate_tests { fn assert_shared_gate_wiring(block: &str, item: &str, actions: &[&str], binds_credentials: bool) { assert_eq!( - block.matches("authorize_admin_request(").count(), + block.matches("authorize_admin_envelope(").count(), 1, "{item} must use exactly one shared gate" ); @@ -2119,7 +2090,7 @@ mod shared_gate_tests { assert!(block.contains(&format!("AdminAction::{action}")), "{item} must authorize with {action}"); } assert_eq!( - block.contains("let cred = authorize_admin_request("), + block.contains("let cred = authorize_admin_envelope("), binds_credentials, "{item} credential binding must match its payload-processing contract" ); diff --git a/rustfs/src/admin/handlers/bucket_recovery.rs b/rustfs/src/admin/handlers/bucket_recovery.rs index e1a748058..e87ebe018 100644 --- a/rustfs/src/admin/handlers/bucket_recovery.rs +++ b/rustfs/src/admin/handlers/bucket_recovery.rs @@ -14,10 +14,10 @@ //! Explicit, generation-fenced recovery for orphaned bucket metadata. -use crate::admin::auth::authorize_admin_request; -use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::auth::authorize_admin_envelope; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::current_object_store_handle; -use crate::admin::storage_api::s3::{Body, S3Request, S3Response}; use crate::server::ADMIN_PREFIX; use crate::site_replication::{site_replication_enabled, with_site_replication_bucket_mutation_lock}; use hyper::{Method, StatusCode}; @@ -27,6 +27,7 @@ use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result}; use serde::{Deserialize, Serialize}; use tracing::info; use uuid::Uuid; +use zeroize::Zeroizing; const EVENT_ADMIN_ORPHAN_BUCKET_RECOVERY: &str = "admin_orphan_bucket_recovery"; const LOG_COMPONENT_ADMIN: &str = "admin"; @@ -58,19 +59,18 @@ pub fn register_bucket_recovery_route(router: &mut S3Router) -> #[async_trait::async_trait] impl Operation for RecoverOrphanedBucketHandler { - async fn call(&self, mut req: S3Request, params: Params<'_, '_>) -> S3Result> { - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::RecoverOrphanedBucketAction)]).await?; + async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result { + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::RecoverOrphanedBucketAction)]).await?; let secret_key = req - .credentials - .as_ref() - .map(|credentials| credentials.secret_key.clone()) + .credentials() + .map(|credentials| Zeroizing::new(credentials.secret_key().to_owned())) .ok_or_else(|| S3Error::with_message(S3ErrorCode::InvalidRequest, "authentication required"))?; let bucket = params .get("bucket") .filter(|bucket| !bucket.is_empty()) .ok_or_else(|| S3Error::with_message(S3ErrorCode::InvalidRequest, "bucket name is required"))?; let body = req - .input + .take_body() .store_all_limited(rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|_| S3Error::with_message(S3ErrorCode::InvalidRequest, "failed to read recovery request body"))?; @@ -123,9 +123,9 @@ impl Operation for RecoverOrphanedBucketHandler { "orphaned bucket metadata recovery completed" ); - super::admin_json_response( - req.uri.path(), - secret_key.expose(), + super::owned_admin_json_response( + req.uri().path(), + &secret_key, StatusCode::OK, &RecoverOrphanedBucketResponse { bucket: bucket.to_owned(), diff --git a/rustfs/src/admin/handlers/cluster_snapshot.rs b/rustfs/src/admin/handlers/cluster_snapshot.rs index 2435c5f10..54628add8 100644 --- a/rustfs/src/admin/handlers/cluster_snapshot.rs +++ b/rustfs/src/admin/handlers/cluster_snapshot.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; use crate::admin::storage_api::cluster::{CapabilityState, CapabilityStatus, ObservabilitySnapshot, TopologySnapshot}; use crate::admin::{ - auth::authorize_admin_request, - router::{AdminOperation, Operation, S3Router}, + auth::authorize_admin_envelope, + router::{AdminOperation, S3Router}, runtime_sources::default_admin_usecase, storage_api::cluster::{ ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot, @@ -29,15 +30,14 @@ use crate::cluster_snapshot::{ cluster_has_actionable_pressure, }; use crate::server::{ADMIN_PREFIX, ReadinessDegradedReason}; +use http::header::CONTENT_TYPE; use http::{HeaderMap, HeaderValue, StatusCode}; use hyper::Method; use matchit::Params; use rustfs_concurrency::AdmissionState as WorkloadAdmissionState; use rustfs_concurrency::{AdmissionState, WorkloadAdmissionRegistrySnapshot, WorkloadAdmissionSnapshot, WorkloadClass}; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Result, s3_error}; -use s3s::header::CONTENT_TYPE; -use s3s::{Body, S3Request, S3Response}; +use rustfs_s3_types::{Body, S3Result, s3_error}; use serde::Serialize; use std::time::{SystemTime, UNIX_EPOCH}; @@ -72,39 +72,35 @@ pub(crate) struct ClusterSnapshotDiscoveryResponse { /// The pre-check keeps this endpoint's historical missing-credentials message; /// the shared gate reports "get cred failed". -async fn authorize_cluster_snapshot_request(req: &S3Request) -> S3Result<()> { - if req.credentials.is_none() { +async fn authorize_cluster_snapshot_request(req: &AdminRequest) -> S3Result<()> { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?; Ok(()) } -fn build_json_response( - status: StatusCode, - body: &impl Serialize, - request_id: Option<&HeaderValue>, -) -> S3Result> { +fn build_json_response(status: StatusCode, body: &impl Serialize, request_id: Option<&HeaderValue>) -> S3Result { let data = serde_json::to_vec(body).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); if let Some(value) = request_id { header.insert("x-request-id", value.clone()); } - Ok(S3Response::with_headers((status, Body::from(data)), header)) + Ok(AdminResponse::with_headers(status, Body::from(data), header)) } pub struct GetClusterSnapshotHandler {} #[async_trait::async_trait] impl Operation for GetClusterSnapshotHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { authorize_cluster_snapshot_request(&req).await?; let snapshot = default_admin_usecase().execute_collect_cluster_read_only_snapshot().await; let server_info_endpoint = crate::runtime_sources::current_local_node_name().await; let snapshot = snapshot.map(|snapshot| ClusterSnapshotView::from_snapshot(snapshot, server_info_endpoint)); - build_json_response(StatusCode::OK, &ClusterSnapshotResponse { snapshot }, req.headers.get("x-request-id")) + build_json_response(StatusCode::OK, &ClusterSnapshotResponse { snapshot }, req.headers().get("x-request-id")) } } @@ -1035,6 +1031,7 @@ mod tests { }; use crate::shared_types::{DependencyReadiness, ReadinessDegradedReason}; use rustfs_concurrency::{AdmissionState, WorkloadAdmissionRegistrySnapshot, WorkloadAdmissionSnapshot, WorkloadClass}; + use rustfs_s3_types::Body; #[test] fn cluster_snapshot_handler_requires_server_info_admin_permission() { @@ -1063,17 +1060,11 @@ mod tests { /// message it has always returned (rustfs/backlog#1829). #[tokio::test] async fn cluster_snapshot_gate_keeps_its_missing_credentials_message() { - let req = s3s::S3Request { - input: s3s::Body::from(String::new()), - method: http::Method::GET, - uri: http::Uri::from_static("/rustfs/admin/v4/cluster/snapshot"), - headers: http::HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; + let req = crate::admin::operation::test_support::TestRequest::new(http::Method::GET, "/rustfs/admin/v4/cluster/snapshot") + .headers(http::HeaderMap::new()) + .extensions(http::Extensions::new()) + .body(Body::from(String::new())) + .build(); let err = super::authorize_cluster_snapshot_request(&req) .await diff --git a/rustfs/src/admin/handlers/idp_compat.rs b/rustfs/src/admin/handlers/idp_compat.rs index 6ca22ec16..f261383b7 100644 --- a/rustfs/src/admin/handlers/idp_compat.rs +++ b/rustfs/src/admin/handlers/idp_compat.rs @@ -45,7 +45,7 @@ use crate::admin::handlers::policies::{ListPolicyEntitiesBuiltin, handle_builtin use crate::admin::handlers::service_account::AddServiceAccount; use crate::admin::handlers::user::ImportIam; use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; -use crate::admin::router::{AdminOperation, Operation as LegacyOperation, S3Router}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::{current_app_context, current_ready_iam_handle, current_server_config_for_context}; use crate::admin::utils::is_compat_admin_request; use crate::auth::{check_key_valid, get_session_token}; @@ -170,15 +170,9 @@ static LIST_ACCESS_KEYS_OPENID_BULK: ListAccessKeysProviderBulk = ListAccessKeys /// the same code path. pub struct ImportIamV2 {} -// Stays on the legacy trait until `user::ImportIam`, which it delegates to, -// serves the owned request. #[async_trait::async_trait] -impl LegacyOperation for ImportIamV2 { - async fn call( - &self, - req: s3s::S3Request, - params: Params<'_, '_>, - ) -> S3Result> { +impl Operation for ImportIamV2 { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { // Identical semantics to v1 import-iam: same auth action, same body // format, same structured result. Delegate to the shared handler. ImportIam {}.call(req, params).await @@ -687,15 +681,9 @@ fn current_active_server_config() -> Option, - params: Params<'_, '_>, - ) -> S3Result> { +impl Operation for AddServiceAccountLdap { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { AddServiceAccount {}.call(req, params).await } } diff --git a/rustfs/src/admin/handlers/kms_audit.rs b/rustfs/src/admin/handlers/kms_audit.rs index bd04a0290..45f12881f 100644 --- a/rustfs/src/admin/handlers/kms_audit.rs +++ b/rustfs/src/admin/handlers/kms_audit.rs @@ -37,8 +37,6 @@ //! [`rustfs_kms::redact_encryption_context`]. use crate::app::RequestEnvelope; -use crate::server::RemoteAddr; -use crate::storage::access::request_context_from_extensions; use crate::storage::helper::spawn_background_with_context; use hashbrown::HashMap; use rustfs_audit::entity::{ApiDetailsBuilder, AuditEntry, AuditEntryBuilder}; @@ -180,28 +178,11 @@ pub(super) struct KmsAdminAudit { } impl KmsAdminAudit { - /// Build the audit state for an authenticated admin request. + /// Build the audit state for an authenticated admin request from the owned + /// request envelope. /// - /// Takes the request in pieces because every KMS handler moves - /// `credentials` out of it before reaching this point. - pub(super) fn from_request(extensions: &http::Extensions, headers: &http::HeaderMap, cred: &Credentials) -> Self { - // The peer address the authorization gate was given, so the audit trail - // and the policy decision agree on where the request came from. - let source_ip = extensions - .get::>() - .and_then(|opt| opt.map(|addr| addr.0.ip().to_string())); - let request_id = request_context_from_extensions(extensions).map(|context| context.request_id); - - Self::new( - &cred.access_key, - &cred.parent_user, - source_ip, - Some(get_request_user_agent(headers)), - request_id, - ) - } - - /// Build the audit state from the owned request envelope. + /// The peer address is the one the authorization gate was given, so the + /// audit trail and the policy decision agree on where the request came from. pub(super) fn from_envelope(envelope: &RequestEnvelope, cred: &Credentials) -> Self { let source_ip = envelope.remote_addr().map(|addr| addr.ip().to_string()); let request_id = envelope.request_context().map(|context| context.request_id.clone()); @@ -217,7 +198,7 @@ impl KmsAdminAudit { /// Assemble the operation context from already-extracted request values. /// - /// Separate from [`Self::from_request`] so the mapping onto an audit entry + /// Separate from [`Self::from_envelope`] so the mapping onto an audit entry /// is testable without an HTTP request. fn new( access_key: &str, @@ -496,12 +477,12 @@ mod tests { } #[test] - fn from_envelope_reads_the_same_facts_as_from_request() { + fn from_envelope_reads_the_request_facts() { let peer: std::net::SocketAddr = "10.1.2.3:4455".parse().expect("socket address"); let mut headers = http::HeaderMap::new(); headers.insert(http::header::USER_AGENT, http::HeaderValue::from_static("rustfs-admin/1")); let mut extensions = http::Extensions::new(); - extensions.insert(Some(RemoteAddr(peer))); + extensions.insert(Some(crate::server::RemoteAddr(peer))); extensions.insert(crate::storage::request_context::RequestContext::from_headers(&headers)); let cred = Credentials { access_key: "AKIAOPERATOR".to_string(), @@ -509,19 +490,14 @@ mod tests { ..Default::default() }; - let from_request = KmsAdminAudit::from_request(&extensions, &headers, &cred); let request = crate::admin::operation::test_support::TestRequest::new(http::Method::POST, "/rustfs/admin/v3/kms/key") .headers(headers) .extensions(extensions) .build(); let from_envelope = KmsAdminAudit::from_envelope(request.envelope(), &cred); - // `operation_id` is minted per context, so compare every fact but it. - let (owned, legacy) = (from_envelope.context(), from_request.context()); - assert_eq!(owned.principal, legacy.principal); - assert_eq!(owned.source_ip, legacy.source_ip); - assert_eq!(owned.user_agent, legacy.user_agent); - assert_eq!(owned.additional_context, legacy.additional_context); + let owned = from_envelope.context(); + assert_eq!(owned.principal, "AKIAOPERATOR"); assert_eq!(owned.source_ip.as_deref(), Some("10.1.2.3")); assert_eq!(owned.user_agent.as_deref(), Some("rustfs-admin/1")); assert_eq!( diff --git a/rustfs/src/admin/handlers/kms_backup.rs b/rustfs/src/admin/handlers/kms_backup.rs index a08b92508..9a17af9e5 100644 --- a/rustfs/src/admin/handlers/kms_backup.rs +++ b/rustfs/src/admin/handlers/kms_backup.rs @@ -42,11 +42,12 @@ use super::kms_audit::{KmsAdminAudit, KmsAdminOperation}; use crate::admin::auth::validate_admin_request; -use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::{current_deployment_id, current_kms_runtime_service_manager}; -use crate::admin::utils::json_response; +use crate::admin::utils::owned_json_response; use crate::auth::{check_key_valid, get_session_token}; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use base64_simd::STANDARD as BASE64; use hyper::{Method, StatusCode}; use matchit::Params; @@ -60,7 +61,6 @@ use rustfs_kms::config::{BackendConfig, KmsConfig, LocalConfig, VaultAuthMethod} use rustfs_kms::{KmsBackend, KmsManager, KmsServiceManager, KmsServiceStatus}; use rustfs_policy::policy::action::{Action, KmsAction}; use rustfs_s3_types::{S3Result, s3_error}; -use s3s::{Body, S3Request, S3Response}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; @@ -897,9 +897,9 @@ struct ErrorResponse { message: String, } -fn failure_response(failure: Failure) -> S3Result> { +fn failure_response(failure: Failure) -> S3Result { let (status, message) = failure; - json_response(status, &ErrorResponse { success: false, message }) + owned_json_response(status, &ErrorResponse { success: false, message }) } /// Authenticate, audit-gate on the dedicated action, and read the body. @@ -907,32 +907,25 @@ fn failure_response(failure: Failure) -> S3Result /// Returns the audit state and the raw body. The gate runs before the body is /// parsed so an unauthorized caller learns nothing about the request contract. async fn authorize( - mut req: S3Request, + mut req: AdminRequest, actions: Vec, operation: KmsAdminOperation, ) -> S3Result<(KmsAdminAudit, Vec)> { - let Some(cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; - let (cred, owner) = check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; - let audit = KmsAdminAudit::from_request(&req.extensions, &req.headers, &cred); + let (cred, owner) = + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; + let audit = KmsAdminAudit::from_envelope(req.envelope(), &cred); audit.gate_admin( - validate_admin_request( - &req.headers, - &cred, - owner, - false, - actions, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await, + validate_admin_request(req.headers(), &cred, owner, false, actions, req.remote_addr()).await, operation, None, )?; let body = req - .input + .take_body() .store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))? @@ -1012,7 +1005,7 @@ pub struct KmsBackupHandler {} #[async_trait::async_trait] impl Operation for KmsBackupHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let (audit, body) = authorize(req, kms_backup_actions(), KmsAdminOperation::Backup).await?; let request: KmsBackupRequest = parse_optional_body(&body)?; @@ -1036,7 +1029,7 @@ impl Operation for KmsBackupHandler { match execute_backup(&manager, &context, &request).await { Ok(response) => { finish(&audit, KmsAdminOperation::Backup, "backup", Some(&response.backup_id), None); - json_response(StatusCode::OK, &response) + owned_json_response(StatusCode::OK, &response) } Err(failure) => { finish( @@ -1057,7 +1050,7 @@ pub struct KmsRestoreDryRunHandler {} #[async_trait::async_trait] impl Operation for KmsRestoreDryRunHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let (audit, body) = authorize(req, kms_restore_actions(), KmsAdminOperation::RestoreDryRun).await?; let request: KmsRestoreDryRunRequest = parse_body(&body)?; @@ -1078,7 +1071,7 @@ impl Operation for KmsRestoreDryRunHandler { match execute_restore_dry_run(&context, &request).await { Ok(response) => { finish(&audit, KmsAdminOperation::RestoreDryRun, "restore_dry_run", Some(&request.bundle), None); - json_response(StatusCode::OK, &response) + owned_json_response(StatusCode::OK, &response) } Err(failure) => { finish( @@ -1099,7 +1092,7 @@ pub struct KmsRestoreHandler {} #[async_trait::async_trait] impl Operation for KmsRestoreHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let (audit, body) = authorize(req, kms_restore_actions(), KmsAdminOperation::Restore).await?; let request: KmsRestoreRequest = parse_body(&body)?; @@ -1114,7 +1107,7 @@ impl Operation for KmsRestoreHandler { match execute_restore(&context, &request).await { Ok(report) => { finish(&audit, KmsAdminOperation::Restore, "restore", Some(&report.backup_id), None); - json_response(StatusCode::OK, &report) + owned_json_response(StatusCode::OK, &report) } Err(failure) => { finish(&audit, KmsAdminOperation::Restore, "restore", Some(&request.bundle), Some(&failure.1)); @@ -1129,7 +1122,7 @@ pub struct KmsRestoreAbortHandler {} #[async_trait::async_trait] impl Operation for KmsRestoreAbortHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let (audit, body) = authorize(req, kms_restore_actions(), KmsAdminOperation::RestoreAbort).await?; let request: KmsRestoreAbortRequest = parse_body(&body)?; @@ -1144,7 +1137,7 @@ impl Operation for KmsRestoreAbortHandler { match execute_restore_abort(&context, &request).await { Ok(response) => { finish(&audit, KmsAdminOperation::RestoreAbort, "restore_abort", None, None); - json_response(StatusCode::OK, &response) + owned_json_response(StatusCode::OK, &response) } Err(failure) => { finish(&audit, KmsAdminOperation::RestoreAbort, "restore_abort", None, Some(&failure.1)); @@ -1159,11 +1152,11 @@ pub struct KmsBackupStatusHandler {} #[async_trait::async_trait] impl Operation for KmsBackupStatusHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let (audit, _body) = authorize(req, kms_backup_actions(), KmsAdminOperation::BackupStatus).await?; let response = backup_status().await; finish(&audit, KmsAdminOperation::BackupStatus, "backup_status", None, None); - json_response(StatusCode::OK, &response) + owned_json_response(StatusCode::OK, &response) } } diff --git a/rustfs/src/admin/handlers/kms_dynamic.rs b/rustfs/src/admin/handlers/kms_dynamic.rs index 35070cfa3..5667c27bf 100644 --- a/rustfs/src/admin/handlers/kms_dynamic.rs +++ b/rustfs/src/admin/handlers/kms_dynamic.rs @@ -16,9 +16,10 @@ use super::kms_audit::{KmsAdminAudit, KmsAdminOperation}; use crate::admin::auth::validate_admin_request; -use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::{ - AppContext, app_context_from_req, current_app_context, current_kms_runtime_service_manager, + AppContext, app_context_from_envelope, current_app_context, current_kms_runtime_service_manager, current_notification_system_for_context, current_object_store_handle_for_context, current_or_init_kms_runtime_service_manager, }; @@ -28,7 +29,7 @@ use crate::admin::storage_api::error::StorageError; use crate::admin::storage_api::runtime::ECStore; use crate::admin::storage_api::s3::{S3ErrorCode, error as admin_s3_error}; use crate::auth::{check_key_valid, get_session_token}; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use hyper::{Method, StatusCode}; use matchit::Params; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; @@ -37,8 +38,7 @@ use rustfs_kms::{ StartKmsResponse, StopKmsResponse, }; use rustfs_policy::policy::action::{Action, KmsAction}; -use rustfs_s3_types::{S3Result, s3_error}; -use s3s::{Body, S3Request, S3Response}; +use rustfs_s3_types::{Body, S3Result, s3_error}; use sha2::{Digest, Sha256}; use std::future::Future; use std::sync::Arc; @@ -731,41 +731,33 @@ pub struct ConfigureKmsHandler; #[async_trait::async_trait] impl Operation for ConfigureKmsHandler { - async fn call(&self, mut req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(cred) = req.credentials else { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let audit = KmsAdminAudit::from_request(&req.extensions, &req.headers, &cred); + let audit = KmsAdminAudit::from_envelope(req.envelope(), &cred); audit.gate_admin( - validate_admin_request( - &req.headers, - &cred, - owner, - false, - kms_configure_actions(), - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await, + validate_admin_request(req.headers(), &cred, owner, false, kms_configure_actions(), req.remote_addr()).await, KmsAdminOperation::Configure, None, )?; let body = req - .input + .take_body() .store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; let mut configure_request: ConfigureKmsRequest = if body.is_empty() { - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::BAD_REQUEST, Body::from("Request body is required".to_string()), - ))); + )); } else { match serde_json::from_slice(&body) { Ok(req) => req, @@ -779,7 +771,7 @@ impl Operation for ConfigureKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}"))))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}")))); } } }; @@ -797,11 +789,11 @@ impl Operation for ConfigureKmsHandler { let existing_config = service_manager.get_config().await; if let Err(e) = normalize_configure_request_secrets(&mut configure_request, existing_config.as_ref()) { - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(e)))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(e))); } if let Err(e) = ensure_kms_request_persistable(&configure_request) { - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(e)))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(e))); } // Convert request to KmsConfig @@ -868,14 +860,14 @@ impl Operation for ConfigureKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::INTERNAL_SERVER_ERROR, Body::from("Serialization error".to_string()), - ))); + )); } }; - Ok(S3Response::new((StatusCode::OK, Body::from(json_response)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json_response))) } } @@ -884,32 +876,24 @@ pub struct StartKmsHandler; #[async_trait::async_trait] impl Operation for StartKmsHandler { - async fn call(&self, mut req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(cred) = req.credentials else { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let audit = KmsAdminAudit::from_request(&req.extensions, &req.headers, &cred); + let audit = KmsAdminAudit::from_envelope(req.envelope(), &cred); audit.gate_admin( - validate_admin_request( - &req.headers, - &cred, - owner, - false, - kms_service_control_actions(), - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await, + validate_admin_request(req.headers(), &cred, owner, false, kms_service_control_actions(), req.remote_addr()).await, KmsAdminOperation::Start, None, )?; let body = req - .input + .take_body() .store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; @@ -929,7 +913,7 @@ impl Operation for StartKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}"))))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}")))); } } }; @@ -1025,14 +1009,14 @@ impl Operation for StartKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::INTERNAL_SERVER_ERROR, Body::from("Serialization error".to_string()), - ))); + )); } }; - Ok(S3Response::new((StatusCode::OK, Body::from(json_response)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json_response))) } } @@ -1041,26 +1025,18 @@ pub struct StopKmsHandler; #[async_trait::async_trait] impl Operation for StopKmsHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(cred) = req.credentials else { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let audit = KmsAdminAudit::from_request(&req.extensions, &req.headers, &cred); + let audit = KmsAdminAudit::from_envelope(req.envelope(), &cred); audit.gate_admin( - validate_admin_request( - &req.headers, - &cred, - owner, - false, - kms_service_control_actions(), - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await, + validate_admin_request(req.headers(), &cred, owner, false, kms_service_control_actions(), req.remote_addr()).await, KmsAdminOperation::Stop, None, )?; @@ -1126,14 +1102,14 @@ impl Operation for StopKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::INTERNAL_SERVER_ERROR, Body::from("Serialization error".to_string()), - ))); + )); } }; - Ok(S3Response::new((StatusCode::OK, Body::from(json_response)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json_response))) } } @@ -1142,27 +1118,19 @@ pub struct ReloadKmsHandler; #[async_trait::async_trait] impl Operation for ReloadKmsHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let context = app_context_from_req(&req) + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let context = app_context_from_envelope(req.envelope()) .ok_or_else(|| admin_s3_error(S3ErrorCode::ServiceUnavailable, "server context is not ready"))?; - let Some(cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(admin_s3_error(S3ErrorCode::InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; - let audit = KmsAdminAudit::from_request(&req.extensions, &req.headers, &cred); + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; + let audit = KmsAdminAudit::from_envelope(req.envelope(), &cred); audit.gate_admin( - validate_admin_request( - &req.headers, - &cred, - owner, - false, - kms_service_control_actions(), - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await, + validate_admin_request(req.headers(), &cred, owner, false, kms_service_control_actions(), req.remote_addr()).await, KmsAdminOperation::Reload, None, )?; @@ -1231,7 +1199,7 @@ impl Operation for ReloadKmsHandler { admin_s3_error(S3ErrorCode::InternalError, "failed to serialize KMS reload response") })?; - Ok(S3Response::new((StatusCode::OK, Body::from(json_response)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json_response))) } } @@ -1240,23 +1208,15 @@ pub struct GetKmsStatusHandler; #[async_trait::async_trait] impl Operation for GetKmsStatusHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(cred) = req.credentials else { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - validate_admin_request( - &req.headers, - &cred, - owner, - false, - kms_service_control_actions(), - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await?; + validate_admin_request(req.headers(), &cred, owner, false, kms_service_control_actions(), req.remote_addr()).await?; info!( component = LOG_COMPONENT_ADMIN, @@ -1315,14 +1275,14 @@ impl Operation for GetKmsStatusHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::INTERNAL_SERVER_ERROR, Body::from("Serialization error".to_string()), - ))); + )); } }; - Ok(S3Response::new((StatusCode::OK, Body::from(json_response)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json_response))) } } @@ -1331,41 +1291,33 @@ pub struct ReconfigureKmsHandler; #[async_trait::async_trait] impl Operation for ReconfigureKmsHandler { - async fn call(&self, mut req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(cred) = req.credentials else { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let audit = KmsAdminAudit::from_request(&req.extensions, &req.headers, &cred); + let audit = KmsAdminAudit::from_envelope(req.envelope(), &cred); audit.gate_admin( - validate_admin_request( - &req.headers, - &cred, - owner, - false, - kms_configure_actions(), - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ) - .await, + validate_admin_request(req.headers(), &cred, owner, false, kms_configure_actions(), req.remote_addr()).await, KmsAdminOperation::Reconfigure, None, )?; let body = req - .input + .take_body() .store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; let mut configure_request: ConfigureKmsRequest = if body.is_empty() { - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::BAD_REQUEST, Body::from("Request body is required".to_string()), - ))); + )); } else { match serde_json::from_slice(&body) { Ok(req) => req, @@ -1379,7 +1331,7 @@ impl Operation for ReconfigureKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}"))))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}")))); } } }; @@ -1397,11 +1349,11 @@ impl Operation for ReconfigureKmsHandler { let existing_config = service_manager.get_config().await; if let Err(e) = normalize_configure_request_secrets(&mut configure_request, existing_config.as_ref()) { - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(e)))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(e))); } if let Err(e) = ensure_kms_request_persistable(&configure_request) { - return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(e)))); + return Ok(AdminResponse::new(StatusCode::BAD_REQUEST, Body::from(e))); } // Convert request to KmsConfig @@ -1469,14 +1421,14 @@ impl Operation for ReconfigureKmsHandler { error = %e, "admin kms dynamic state" ); - return Ok(S3Response::new(( + return Ok(AdminResponse::new( StatusCode::INTERNAL_SERVER_ERROR, Body::from("Serialization error".to_string()), - ))); + )); } }; - Ok(S3Response::new((StatusCode::OK, Body::from(json_response)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json_response))) } } diff --git a/rustfs/src/admin/handlers/mod.rs b/rustfs/src/admin/handlers/mod.rs index b919a626a..cca8e181a 100644 --- a/rustfs/src/admin/handlers/mod.rs +++ b/rustfs/src/admin/handlers/mod.rs @@ -85,24 +85,6 @@ pub mod user_policy_binding; /// body; `encode_compatible_admin_payload` decides that from the path, so every /// handler must go through here rather than serializing directly, or a /// MinIO client gets plaintext where it expects ciphertext. -pub(crate) fn admin_json_response( - path: &str, - secret_key: &str, - status: http::StatusCode, - payload: &T, -) -> rustfs_s3_types::S3Result> { - let body = serde_json::to_vec(payload).map_err(|e| { - rustfs_s3_types::S3Error::with_message(rustfs_s3_types::S3ErrorCode::InternalError, format!("serialize error: {e}")) - })?; - let (body, content_type) = crate::admin::utils::encode_compatible_admin_payload(path, secret_key, body)?; - - let mut header = hyper::HeaderMap::new(); - header.insert(s3s::header::CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(s3s::S3Response::with_headers((status, s3s::Body::from(body)), header)) -} - -/// Owned-boundary form of [`admin_json_response`]: the same compat-prefix -/// encryption decision, `Content-Type`, and bytes, as an `AdminResponse`. pub(crate) fn owned_admin_json_response( path: &str, secret_key: &str, @@ -141,37 +123,36 @@ mod tests { use super::*; #[tokio::test] - async fn owned_admin_json_response_matches_the_legacy_shape_on_both_prefixes() { + async fn owned_admin_json_response_encrypts_only_on_the_compat_prefix() { let payload = serde_json::json!({"users": ["alice", "bob"]}); + let plain = serde_json::to_vec(&payload).expect("payload serializes"); - let mut legacy = admin_json_response("/rustfs/admin/v3/list-users", "secret", http::StatusCode::OK, &payload) - .expect("legacy serializes"); let (status, headers, mut body) = owned_admin_json_response("/rustfs/admin/v3/list-users", "secret", http::StatusCode::OK, &payload) - .expect("owned serializes") + .expect("serializes") .into_parts(); - assert_eq!(status, legacy.output.0); - assert_eq!(headers, legacy.headers); + assert_eq!(status, http::StatusCode::OK); + assert_eq!(headers.len(), 1); assert_eq!( - body.store_all_limited(4096).await.expect("owned body reads"), - legacy.output.1.store_all_limited(4096).await.expect("legacy body reads") + headers.get(http::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()), + Some("application/json") ); + assert_eq!(body.store_all_limited(4096).await.expect("body reads").as_ref(), plain.as_slice()); - let legacy = admin_json_response("/minio/admin/v3/list-users", "secret", http::StatusCode::OK, &payload) - .expect("legacy serializes compat payload"); let (status, headers, mut body) = owned_admin_json_response("/minio/admin/v3/list-users", "secret", http::StatusCode::OK, &payload) - .expect("owned serializes compat payload") + .expect("serializes compat payload") .into_parts(); - assert_eq!(status, legacy.output.0); - assert_eq!(headers, legacy.headers); + assert_eq!(status, http::StatusCode::OK); + assert_eq!(headers.len(), 1); assert_eq!( headers.get(http::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()), Some("application/octet-stream") ); - let encrypted = body.store_all_limited(4096).await.expect("owned compat body reads"); - let decrypted = rustfs_crypto::decrypt_stream_io(b"secret", &encrypted).expect("owned compat payload decrypts"); - assert_eq!(decrypted, serde_json::to_vec(&payload).expect("payload serializes")); + let encrypted = body.store_all_limited(4096).await.expect("compat body reads"); + assert_ne!(encrypted.as_ref(), plain.as_slice()); + let decrypted = rustfs_crypto::decrypt_stream_io(b"secret", &encrypted).expect("compat payload decrypts"); + assert_eq!(decrypted, plain); } #[test] diff --git a/rustfs/src/admin/handlers/module_switch.rs b/rustfs/src/admin/handlers/module_switch.rs index 9126cedd7..20de924ce 100644 --- a/rustfs/src/admin/handlers/module_switch.rs +++ b/rustfs/src/admin/handlers/module_switch.rs @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::runtime_sources::{AppContext, app_context_from_req, default_admin_usecase}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::runtime_sources::{AppContext, app_context_from_envelope, default_admin_usecase}; use crate::admin::service::config::{ preflight_dynamic_config_reload_for_context, signal_dynamic_config_reload_checked_for_context, }; use crate::admin::{ - auth::authorize_admin_request, + auth::authorize_admin_envelope, handlers::supervise_admin_mutation, - router::{AdminOperation, Operation, S3Router}, + router::{AdminOperation, S3Router}, }; use crate::server::{ ADMIN_PREFIX, MODULE_SWITCHES_SIGNAL_SUBSYSTEM, ModuleSwitchSnapshot, ModuleSwitchSource, PersistedModuleSwitches, @@ -28,13 +29,13 @@ use crate::server::{ refresh_persisted_module_switches_from, refresh_persisted_module_switches_from_store, save_persisted_module_switches_to, validate_module_switch_update, with_notify_runtime_reconcile_lock, }; +use http::header::CONTENT_TYPE; use http::{HeaderMap, StatusCode}; use hyper::Method; use matchit::Params; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Result, s3_error}; -use s3s::{Body, S3Request, S3Response, header::CONTENT_TYPE}; +use rustfs_s3_types::{Body, S3Result, s3_error}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -100,28 +101,24 @@ impl From for ModuleSwitchesResponse { } } -fn build_response( - status: StatusCode, - body: &T, - request_id: Option<&http::HeaderValue>, -) -> S3Result> { +fn build_response(status: StatusCode, body: &T, request_id: Option<&http::HeaderValue>) -> S3Result { let data = serde_json::to_vec(body).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().unwrap()); if let Some(v) = request_id { header.insert("x-request-id", v.clone()); } - Ok(S3Response::with_headers((status, Body::from(data)), header)) + Ok(AdminResponse::with_headers(status, Body::from(data), header)) } /// The pre-check keeps these endpoints' historical missing-credentials message; /// the shared gate reports "get cred failed". -async fn authorize_module_switch_request(req: &S3Request, action: AdminAction) -> S3Result<()> { - if req.credentials.is_none() { +async fn authorize_module_switch_request(req: &AdminRequest, action: AdminAction) -> S3Result<()> { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(req, vec![Action::AdminAction(action)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(action)]).await?; Ok(()) } @@ -213,10 +210,10 @@ pub struct GetModuleSwitchesHandler {} #[async_trait::async_trait] impl Operation for GetModuleSwitchesHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { authorize_module_switch_request(&req, AdminAction::ServerInfoAdminAction).await?; let snapshot = refresh_module_switch_snapshot().await?; - build_response(StatusCode::OK, &ModuleSwitchesResponse::from(snapshot), req.headers.get("x-request-id")) + build_response(StatusCode::OK, &ModuleSwitchesResponse::from(snapshot), req.headers().get("x-request-id")) } } @@ -224,16 +221,17 @@ pub struct UpdateModuleSwitchesHandler {} #[async_trait::async_trait] impl Operation for UpdateModuleSwitchesHandler { - async fn call(&self, mut req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { authorize_module_switch_request(&req, AdminAction::ConfigUpdateAdminAction).await?; - let context = app_context_from_req(&req).ok_or_else(|| s3_error!(InternalError, "storage layer not initialized"))?; + let context = + app_context_from_envelope(req.envelope()).ok_or_else(|| s3_error!(InternalError, "storage layer not initialized"))?; let store = context.object_store(); refresh_persisted_module_switches_from(store.clone()) .await .map_err(|e| s3_error!(InternalError, "failed to reload persisted module switches: {}", e))?; let body = req - .input + .take_body() .store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; @@ -258,30 +256,25 @@ impl Operation for UpdateModuleSwitchesHandler { supervise_admin_mutation("module switch update", apply_module_switch_update(context, switches)).await?; let snapshot = current_module_switch_snapshot(); - build_response(StatusCode::OK, &ModuleSwitchesResponse::from(snapshot), req.headers.get("x-request-id")) + build_response(StatusCode::OK, &ModuleSwitchesResponse::from(snapshot), req.headers().get("x-request-id")) } } #[cfg(test)] mod tests { use super::{ModuleSwitchDiscovery, ModuleSwitchSource, ModuleSwitchesResponse}; + use rustfs_s3_types::Body; /// These endpoints authorize through the shared admin gate, which reports /// "get cred failed" for a credential-less request. The pre-check keeps the /// message they have always returned (rustfs/backlog#1829). #[tokio::test] async fn module_switch_gate_keeps_its_missing_credentials_message() { - let req = s3s::S3Request { - input: s3s::Body::from(String::new()), - method: http::Method::GET, - uri: http::Uri::from_static("/rustfs/admin/v3/module-switches"), - headers: http::HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; + let req = crate::admin::operation::test_support::TestRequest::new(http::Method::GET, "/rustfs/admin/v3/module-switches") + .headers(http::HeaderMap::new()) + .extensions(http::Extensions::new()) + .body(Body::from(String::new())) + .build(); let err = super::authorize_module_switch_request(&req, rustfs_policy::policy::action::AdminAction::ServerInfoAdminAction) .await diff --git a/rustfs/src/admin/handlers/oidc.rs b/rustfs/src/admin/handlers/oidc.rs index 3e4796292..f9f69f998 100644 --- a/rustfs/src/admin/handlers/oidc.rs +++ b/rustfs/src/admin/handlers/oidc.rs @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::auth::authorize_admin_request; -use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::auth::authorize_admin_envelope; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::current_federated_identity_service; use crate::admin::service::federated_identity::DefaultFederatedSessionBinding; use crate::admin::service::oidc_config; -use crate::admin::utils::json_response; +use crate::admin::utils::owned_json_response; use crate::server::{ADMIN_PREFIX, MINIO_ADMIN_PREFIX, console_prefix}; use http::StatusCode; use hyper::Method; @@ -26,8 +27,7 @@ use rustfs_config::{ENV_RUSTFS_BROWSER_REDIRECT_URL, MAX_ADMIN_REQUEST_BODY_SIZE use rustfs_iam::federation::{FederatedSessionBindingError, FederationError}; use rustfs_iam::oidc::{OidcProviderConfigInput, OidcProviderValidationInput, console_session_duration_secs}; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; -use s3s::{Body, S3Request, S3Response}; +use rustfs_s3_types::{Body, S3Error, S3ErrorCode, S3Result, s3_error}; use serde::Serialize; use serde::de::DeserializeOwned; use time::OffsetDateTime; @@ -204,17 +204,16 @@ pub struct ListOidcProvidersHandler {} #[async_trait::async_trait] impl Operation for ListOidcProvidersHandler { - async fn call(&self, _req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, _req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let federation = current_federated_identity_service().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; let providers = federation.list_visible_providers(); let json_body = serde_json::to_vec(&providers) .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("serialize error: {e}")))?; - let mut resp = S3Response::new((StatusCode::OK, Body::from(json_body))); - resp.headers - .insert(http::header::CONTENT_TYPE, http::HeaderValue::from_static("application/json")); - Ok(resp) + let mut headers = http::HeaderMap::new(); + headers.insert(http::header::CONTENT_TYPE, http::HeaderValue::from_static("application/json")); + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(json_body), headers)) } } @@ -222,7 +221,7 @@ pub struct GetOidcConfigHandler {} #[async_trait::async_trait] impl Operation for GetOidcConfigHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { authorize_oidc_config_request(&req, AdminAction::ServerInfoAdminAction).await?; let config = oidc_config::list_config().await.map_err(config_service_error)?; @@ -255,7 +254,7 @@ impl Operation for GetOidcConfigHandler { }) .collect(); - json_response( + owned_json_response( StatusCode::OK, &OidcConfigListResponse { providers, @@ -269,7 +268,7 @@ pub struct PutOidcConfigHandler {} #[async_trait::async_trait] impl Operation for PutOidcConfigHandler { - async fn call(&self, mut req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result { authorize_oidc_config_request(&req, AdminAction::ConfigUpdateAdminAction).await?; let provider_id = params @@ -283,7 +282,7 @@ impl Operation for PutOidcConfigHandler { .await .map_err(config_service_error)?; - json_response( + owned_json_response( StatusCode::OK, &OidcMutationResponse { success: true, @@ -298,7 +297,7 @@ pub struct DeleteOidcConfigHandler {} #[async_trait::async_trait] impl Operation for DeleteOidcConfigHandler { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { authorize_oidc_config_request(&req, AdminAction::ConfigUpdateAdminAction).await?; let provider_id = params @@ -308,7 +307,7 @@ impl Operation for DeleteOidcConfigHandler { let provider_id = provider_id.to_owned(); oidc_config::delete_config(provider_id).await.map_err(config_service_error)?; - json_response( + owned_json_response( StatusCode::OK, &OidcMutationResponse { success: true, @@ -323,13 +322,13 @@ pub struct ValidateOidcConfigHandler {} #[async_trait::async_trait] impl Operation for ValidateOidcConfigHandler { - async fn call(&self, mut req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { authorize_oidc_config_request(&req, AdminAction::ServerInfoAdminAction).await?; let request: OidcProviderValidationInput = parse_json_body(&mut req).await?; let validation = oidc_config::validate_config(request).await.map_err(config_service_error)?; - json_response( + owned_json_response( StatusCode::OK, &OidcValidationResponse { valid: true, @@ -348,7 +347,7 @@ pub struct OidcAuthorizeHandler {} #[async_trait::async_trait] impl Operation for OidcAuthorizeHandler { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { let provider_id = params .get("provider_id") .ok_or_else(|| s3_error!(InvalidRequest, "missing provider_id"))?; @@ -363,7 +362,7 @@ impl Operation for OidcAuthorizeHandler { let redirect_uri = derive_callback_uri(&req, provider_id)?; // Optional: redirect_after query parameter (must be a safe relative path) - 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 auth_url = federation @@ -397,14 +396,14 @@ impl Operation for OidcAuthorizeHandler { ); // Return 302 redirect - let mut resp = S3Response::new((StatusCode::FOUND, Body::empty())); - resp.headers.insert( + let mut headers = http::HeaderMap::new(); + headers.insert( http::header::LOCATION, auth_url .parse() .map_err(|_| s3_error!(InternalError, "failed to construct authorization URL"))?, ); - Ok(resp) + Ok(AdminResponse::with_headers(StatusCode::FOUND, Body::empty(), headers)) } } @@ -414,7 +413,7 @@ pub struct OidcCallbackHandler {} #[async_trait::async_trait] impl Operation for OidcCallbackHandler { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { let provider_id = params .get("provider_id") .ok_or_else(|| s3_error!(InvalidRequest, "missing provider_id"))?; @@ -424,7 +423,7 @@ impl Operation for OidcCallbackHandler { } // Check for error response from IdP - if let Some((error, desc)) = extract_idp_callback_error(&req.uri) { + if let Some((error, desc)) = extract_idp_callback_error(req.uri()) { warn!( event = EVENT_ADMIN_OIDC_STATE, component = LOG_COMPONENT_ADMIN, @@ -443,9 +442,9 @@ impl Operation for OidcCallbackHandler { // Extract code and state from query parameters let code = - extract_query_param(&req.uri, "code").ok_or_else(|| s3_error!(InvalidRequest, "missing 'code' query parameter"))?; - let state = - extract_query_param(&req.uri, "state").ok_or_else(|| s3_error!(InvalidRequest, "missing 'state' query parameter"))?; + extract_query_param(req.uri(), "code").ok_or_else(|| s3_error!(InvalidRequest, "missing 'code' query parameter"))?; + let state = extract_query_param(req.uri(), "state") + .ok_or_else(|| s3_error!(InvalidRequest, "missing 'state' query parameter"))?; let federation = current_federated_identity_service().ok_or_else(|| s3_error!(InternalError, "OIDC not initialized"))?; @@ -516,14 +515,14 @@ impl Operation for OidcCallbackHandler { Some(login.logout_token.as_str()), )?; - let mut resp = S3Response::new((StatusCode::FOUND, Body::empty())); - resp.headers.insert( + let mut headers = http::HeaderMap::new(); + headers.insert( http::header::LOCATION, console_redirect .parse() .map_err(|_| s3_error!(InternalError, "failed to construct console redirect URL"))?, ); - Ok(resp) + Ok(AdminResponse::with_headers(StatusCode::FOUND, Body::empty(), headers)) } } @@ -534,9 +533,9 @@ pub struct OidcLogoutHandler {} #[async_trait::async_trait] impl Operation for OidcLogoutHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let fallback_location = build_console_login_redirect(&req)?; - let Some(logout_token) = extract_query_param(&req.uri, "logout_token") else { + let Some(logout_token) = extract_query_param(req.uri(), "logout_token") else { return redirect_response(&fallback_location); }; @@ -567,14 +566,14 @@ impl Operation for OidcLogoutHandler { /// Uses the provider's configured redirect_uri if set, otherwise derives dynamically /// from request headers. For production deployments behind a reverse proxy, configuring /// an explicit redirect_uri is recommended to prevent header manipulation. -fn derive_callback_uri(req: &S3Request, provider_id: &str) -> S3Result { +fn derive_callback_uri(req: &AdminRequest, provider_id: &str) -> S3Result { let policy = current_federated_identity_service().and_then(|federation| federation.redirect_policy(provider_id)); derive_callback_uri_with_redirect_policy(req, provider_id, policy.as_ref()) } fn derive_callback_uri_with_redirect_policy( - req: &S3Request, + req: &AdminRequest, provider_id: &str, policy: Option<&rustfs_iam::federation::FederatedRedirectPolicy>, ) -> S3Result { @@ -676,7 +675,7 @@ fn build_console_callback_fragment( /// Build the console redirect URL with STS credentials in the hash fragment. fn build_console_redirect( - req: &S3Request, + req: &AdminRequest, access_key: &str, secret_key: &str, session_token: &str, @@ -699,7 +698,7 @@ fn build_console_redirect( Ok(format!("{scheme}://{host}{callback_path}#{fragment}")) } -fn build_console_login_redirect(req: &S3Request) -> S3Result { +fn build_console_login_redirect(req: &AdminRequest) -> S3Result { let console_prefix = console_prefix(); let login_path = format!("{console_prefix}{CONSOLE_LOGIN_SUFFIX}"); if let Some(url) = browser_redirect_url(&login_path)? { @@ -711,31 +710,31 @@ fn build_console_login_redirect(req: &S3Request) -> S3Result { Ok(format!("{scheme}://{host}{login_path}")) } -fn redirect_response(location: &str) -> S3Result> { - let mut resp = S3Response::new((StatusCode::FOUND, Body::empty())); - resp.headers.insert( +fn redirect_response(location: &str) -> S3Result { + let mut headers = http::HeaderMap::new(); + headers.insert( http::header::LOCATION, location .parse() .map_err(|_| s3_error!(InternalError, "failed to construct redirect URL"))?, ); - Ok(resp) + Ok(AdminResponse::with_headers(StatusCode::FOUND, Body::empty(), headers)) } /// The pre-check keeps this endpoint family's historical missing-credentials /// message; the shared gate reports "get cred failed". -async fn authorize_oidc_config_request(req: &S3Request, action: AdminAction) -> S3Result<()> { - if req.credentials.is_none() { +async fn authorize_oidc_config_request(req: &AdminRequest, action: AdminAction) -> S3Result<()> { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(req, vec![Action::AdminAction(action)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(action)]).await?; Ok(()) } -async fn parse_json_body(req: &mut S3Request) -> S3Result { +async fn parse_json_body(req: &mut AdminRequest) -> S3Result { let body = req - .input + .take_body() .store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; @@ -747,15 +746,15 @@ async fn parse_json_body(req: &mut S3Request) -> S3Re serde_json::from_slice(&body).map_err(|e| s3_error!(InvalidRequest, "invalid JSON: {}", e)) } -fn extract_request_scheme(req: &S3Request) -> S3Result { +fn extract_request_scheme(req: &AdminRequest) -> S3Result { let raw_scheme = req - .headers + .headers() .get("x-forwarded-proto") .and_then(|v| v.to_str().ok()) .map(|v| v.split(',').next().map(str::trim).unwrap_or("")) .filter(|v| !v.is_empty()) .map(ToOwned::to_owned) - .or_else(|| req.uri.scheme_str().map(str::to_owned)) + .or_else(|| req.uri().scheme_str().map(str::to_owned)) .unwrap_or_else(|| "http".to_owned()) .to_ascii_lowercase(); @@ -766,16 +765,16 @@ fn extract_request_scheme(req: &S3Request) -> S3Result { Ok(raw_scheme) } -fn extract_request_host(req: &S3Request) -> S3Result { +fn extract_request_host(req: &AdminRequest) -> S3Result { let host = req - .headers + .headers() .get(http::header::HOST) .and_then(|v| v.to_str().ok()) .and_then(|v| { let v = v.trim(); if v.is_empty() { None } else { Some(v.to_owned()) } }) - .or_else(|| req.uri.authority().map(|a| a.as_str().to_owned())) + .or_else(|| req.uri().authority().map(|a| a.as_str().to_owned())) .ok_or_else(|| s3_error!(InvalidRequest, "cannot determine host for redirect URI"))?; parse_host_authority(&host) @@ -908,17 +907,19 @@ mod tests { for (hidden, complete) in [(true, false), (false, false), (false, true)] { let document = serde_json::json!({"provider_id": "workload", "client_id": "rustfs-test", "issuer": base, "config_url": format!("{base}{}/.well-known/openid-configuration", if complete { "/complete" } else { "" }), "hide_from_ui": hidden}); - let request = || { - let mut req = build_oidc_request("/rustfs/admin/v3/oidc/validate", None, None); - req.method = Method::POST; - req.input = Body::from(document.to_string()); - req + let request = |credentials: Option<(&str, &str)>| { + let request = crate::admin::operation::test_support::TestRequest::new(Method::POST, "/rustfs/admin/v3/oidc/validate") + .body(Body::from(document.to_string())); + match credentials { + Some((access_key, secret_key)) => request.credentials(access_key, secret_key, None), + None => request, + } + .build() }; - let denied = ValidateOidcConfigHandler {}.call(request(), Params::new()).await.unwrap_err(); + let denied = ValidateOidcConfigHandler {}.call(request(None), Params::new()).await.unwrap_err(); assert_eq!(denied.code(), &S3ErrorCode::InvalidRequest); assert_eq!(denied.message(), Some("authentication required")); - let mut req = request(); - req.credentials = Some(s3s::auth::Credentials { access_key: "OIDCVALIDATEROOT".into(), secret_key: "oidcValidateRootSecret123".into() }); + let req = request(Some(("OIDCVALIDATEROOT", "oidcValidateRootSecret123"))); let result = ValidateOidcConfigHandler {}.call(req, Params::new()).await; if !hidden && !complete { let err = result.unwrap_err(); @@ -926,7 +927,7 @@ mod tests { assert!(err.message().unwrap().contains("authorization_endpoint")); continue; } - let (status, body) = result.unwrap().output; + let (status, _headers, body) = result.unwrap().into_parts(); assert_eq!(status, StatusCode::OK); let body = body.collect().await.unwrap().to_bytes(); let response: serde_json::Value = serde_json::from_slice(&body).unwrap(); @@ -945,11 +946,7 @@ mod tests { ).await; } - fn build_oidc_request( - uri: &'static str, - host: Option<&'static str>, - forwarded_proto: Option<&'static str>, - ) -> S3Request { + fn build_oidc_request(uri: &'static str, host: Option<&'static str>, forwarded_proto: Option<&'static str>) -> AdminRequest { let mut headers = HeaderMap::new(); if let Some(host) = host { headers.insert(http::header::HOST, HeaderValue::from_static(host)); @@ -958,17 +955,11 @@ mod tests { headers.insert("x-forwarded-proto", HeaderValue::from_static(proto)); } - S3Request { - input: Body::empty(), - method: Method::GET, - uri: Uri::from_static(uri), - headers, - extensions: Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + crate::admin::operation::test_support::TestRequest::new(Method::GET, Uri::from_static(uri)) + .headers(headers) + .extensions(Extensions::new()) + .body(Body::empty()) + .build() } fn test_redirect_policy( @@ -1360,16 +1351,16 @@ mod tests { .0; assert_eq!( - wrapper.matches("authorize_admin_request(").count(), + wrapper.matches("authorize_admin_envelope(").count(), 1, "the OIDC config gate must use exactly one shared gate" ); assert!( - wrapper.contains("authorize_admin_request(req, vec![Action::AdminAction(action)])"), + wrapper.contains("authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(action)])"), "the OIDC config gate must forward its parameterized action unchanged" ); assert!( - !wrapper.contains("let cred = authorize_admin_request("), + !wrapper.contains("let cred = authorize_admin_envelope("), "the OIDC config gate does not need the authenticated credentials" ); diff --git a/rustfs/src/admin/handlers/pools.rs b/rustfs/src/admin/handlers/pools.rs index 245aa35ce..9b504e48c 100644 --- a/rustfs/src/admin/handlers/pools.rs +++ b/rustfs/src/admin/handlers/pools.rs @@ -12,16 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::runtime_sources::object_store_from_extensions; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::runtime_sources::object_store_from_envelope; +use http::header::CONTENT_TYPE; use http::{HeaderMap, HeaderValue, StatusCode, Uri}; use matchit::Params; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; +use rustfs_s3_types::{Body, S3Error, S3ErrorCode, S3Result, s3_error}; use rustfs_utils::{ MaskedAccessKey, http::{AMZ_REQUEST_ID, REQUEST_ID_HEADER}, }; -use s3s::{Body, S3Request, S3Response, header::CONTENT_TYPE}; use serde::Deserialize; use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; @@ -31,12 +32,12 @@ use crate::{ AdminPoolStatus, QueryPoolStatusRequest, current_endpoints_handle, current_notification_system, default_admin_usecase, }, admin::{ - auth::authorize_admin_request, - router::{AdminOperation, Operation, S3Router}, + auth::authorize_admin_envelope, + router::{AdminOperation, S3Router}, storage_api::runtime::{EndpointServerPools, PeerRestClient}, }, error::ApiError, - server::{ADMIN_PREFIX, RemoteAddr}, + server::ADMIN_PREFIX, }; use hyper::Method; @@ -73,10 +74,8 @@ fn admin_request_id(headers: &HeaderMap) -> Option<&str> { .and_then(|value| value.to_str().ok()) } -fn admin_remote_addr(req: &S3Request) -> Option { - req.extensions - .get::>() - .and_then(|opt| opt.map(|addr| addr.0.to_string())) +fn admin_remote_addr(req: &AdminRequest) -> Option { + req.remote_addr().map(|addr| addr.to_string()) } fn log_pool_request_rejected_with_context(operation: &str, reason: &str, request_id: &str, actor: &str, remote_addr: &str) { @@ -532,13 +531,13 @@ pub struct ListPools {} impl Operation for ListPools { // GET //pools/list #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(pool_admin_missing_credentials_error("list pools")); } - authorize_admin_request( - &req, + authorize_admin_envelope( + req.envelope(), vec![ Action::AdminAction(AdminAction::ServerInfoAdminAction), Action::AdminAction(AdminAction::DecommissionAdminAction), @@ -558,7 +557,7 @@ impl Operation for ListPools { header.insert(CONTENT_TYPE, "application/json".parse().unwrap()); log_pool_response_emitted!("list_pools"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -622,13 +621,13 @@ pub struct StatusPool {} impl Operation for StatusPool { // GET //pools/status?pool=http://server{1...4}/disk{1...4} #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(pool_admin_missing_credentials_error("load pool status")); } - authorize_admin_request( - &req, + authorize_admin_envelope( + req.envelope(), vec![ Action::AdminAction(AdminAction::ServerInfoAdminAction), Action::AdminAction(AdminAction::DecommissionAdminAction), @@ -636,7 +635,7 @@ impl Operation for StatusPool { ) .await?; - let query = parse_status_pool_query(&req.uri).map_err(|_| pool_admin_query_parse_error("load pool status"))?; + let query = parse_status_pool_query(req.uri()).map_err(|_| pool_admin_query_parse_error("load pool status"))?; let usecase = default_admin_usecase(); let pools_status = usecase @@ -660,7 +659,7 @@ impl Operation for StatusPool { header.insert(CONTENT_TYPE, "application/json".parse().unwrap()); log_pool_response_emitted!("query_pool_status"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -670,13 +669,13 @@ pub struct StatusDecommission {} impl Operation for StatusDecommission { // GET //decommission/status[?pool=http://server{1...4}/disk{1...4}] #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(pool_admin_missing_credentials_error("load decommission status")); } - authorize_admin_request( - &req, + authorize_admin_envelope( + req.envelope(), vec![ Action::AdminAction(AdminAction::ServerInfoAdminAction), Action::AdminAction(AdminAction::DecommissionAdminAction), @@ -684,7 +683,7 @@ impl Operation for StatusDecommission { ) .await?; - let query = parse_status_pool_query(&req.uri).map_err(|_| pool_admin_query_parse_error("load decommission status"))?; + let query = parse_status_pool_query(req.uri()).map_err(|_| pool_admin_query_parse_error("load decommission status"))?; let usecase = default_admin_usecase(); let data = if query.pool.is_empty() { @@ -709,7 +708,7 @@ impl Operation for StatusDecommission { header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_pool_response_emitted!("query_decommission_status"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -719,8 +718,8 @@ pub struct StartDecommission {} impl Operation for StartDecommission { // POST //pools/decommission?pool=http://server{1...4}/disk{1...4} #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string(); + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let request_id = admin_request_id(req.headers()).unwrap_or_default().to_string(); let remote_addr = admin_remote_addr(&req).unwrap_or_default(); info!( event = EVENT_ADMIN_REQUEST_STATE, @@ -734,16 +733,16 @@ impl Operation for StartDecommission { "admin pool request state" ); - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(pool_admin_missing_credentials_error_with_request( "start decommission", &request_id, &remote_addr, )); }; - let actor = MaskedAccessKey(&input_cred.access_key).to_string(); + let actor = MaskedAccessKey(input_cred.access_key()).to_string(); - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::DecommissionAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::DecommissionAdminAction)]).await?; let audit = PoolAuditContext::new(&request_id, &actor, &remote_addr); let Some(endpoints) = endpoints_from_context() else { @@ -762,11 +761,11 @@ impl Operation for StartDecommission { return Err(s3_error!(NotImplemented)); } - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(decommission_admin_not_initialized_error_with_audit("start decommission", audit)); }; - let query = parse_mutation_pool_query(&req.uri) + let query = parse_mutation_pool_query(req.uri()) .map_err(|_| pool_admin_query_parse_error_with_audit("start decommission", audit))?; let is_byid = query.by_id.as_str() == "true"; @@ -879,7 +878,7 @@ impl Operation for StartDecommission { pool_indices = ?pools_indices, "admin response emitted" ); - Ok(S3Response::new((StatusCode::OK, Body::default()))) + Ok(AdminResponse::new(StatusCode::OK, Body::default())) } } @@ -889,8 +888,8 @@ pub struct CancelDecommission {} impl Operation for CancelDecommission { // POST //pools/cancel?pool=http://server{1...4}/disk{1...4} #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string(); + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let request_id = admin_request_id(req.headers()).unwrap_or_default().to_string(); let remote_addr = admin_remote_addr(&req).unwrap_or_default(); info!( event = EVENT_ADMIN_REQUEST_STATE, @@ -904,16 +903,16 @@ impl Operation for CancelDecommission { "admin pool request state" ); - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(pool_admin_missing_credentials_error_with_request( "cancel decommission", &request_id, &remote_addr, )); }; - let actor = MaskedAccessKey(&input_cred.access_key).to_string(); + let actor = MaskedAccessKey(input_cred.access_key()).to_string(); - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::DecommissionAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::DecommissionAdminAction)]).await?; let audit = PoolAuditContext::new(&request_id, &actor, &remote_addr); let Some(endpoints) = endpoints_from_context() else { @@ -932,7 +931,7 @@ impl Operation for CancelDecommission { return Err(s3_error!(NotImplemented)); } - let query = parse_mutation_pool_query(&req.uri) + let query = parse_mutation_pool_query(req.uri()) .map_err(|_| pool_admin_query_parse_error_with_audit("cancel decommission", audit))?; let is_byid = query.by_id.as_str() == "true"; @@ -956,7 +955,7 @@ impl Operation for CancelDecommission { .map_err(ApiError::from) .map_err(|err| contextualize_admin_pool_api_error(err, "cancel decommission", format!("pool {idx}")))?; } else { - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(decommission_admin_not_initialized_error_with_audit("cancel decommission", audit)); }; @@ -980,7 +979,7 @@ impl Operation for CancelDecommission { pool_index = idx, "admin response emitted" ); - Ok(S3Response::new((StatusCode::OK, Body::default()))) + Ok(AdminResponse::new(StatusCode::OK, Body::default())) } } @@ -991,8 +990,8 @@ impl Operation for ClearDecommission { // POST //pools/clear?pool=http://server{1...4}/disk{1...4} // Clears failed/canceled decommission metadata only; already moved data is not rolled back. #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string(); + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let request_id = admin_request_id(req.headers()).unwrap_or_default().to_string(); let remote_addr = admin_remote_addr(&req).unwrap_or_default(); info!( event = EVENT_ADMIN_REQUEST_STATE, @@ -1006,16 +1005,16 @@ impl Operation for ClearDecommission { "admin pool request state" ); - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(pool_admin_missing_credentials_error_with_request( "clear decommission", &request_id, &remote_addr, )); }; - let actor = MaskedAccessKey(&input_cred.access_key).to_string(); + let actor = MaskedAccessKey(input_cred.access_key()).to_string(); - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::DecommissionAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::DecommissionAdminAction)]).await?; let audit = PoolAuditContext::new(&request_id, &actor, &remote_addr); let Some(endpoints) = endpoints_from_context() else { @@ -1034,7 +1033,7 @@ impl Operation for ClearDecommission { return Err(s3_error!(NotImplemented)); } - let query = parse_mutation_pool_query(&req.uri) + let query = parse_mutation_pool_query(req.uri()) .map_err(|_| pool_admin_query_parse_error_with_audit("clear decommission", audit))?; let is_byid = query.by_id.as_str() == "true"; @@ -1058,7 +1057,7 @@ impl Operation for ClearDecommission { .map_err(ApiError::from) .map_err(|err| contextualize_admin_pool_api_error(err, "clear decommission", format!("pool {idx}")))?; } else { - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(decommission_admin_not_initialized_error_with_audit("clear decommission", audit)); }; @@ -1082,7 +1081,7 @@ impl Operation for ClearDecommission { pool_index = idx, "admin response emitted" ); - Ok(S3Response::new((StatusCode::OK, Body::default()))) + Ok(AdminResponse::new(StatusCode::OK, Body::default())) } } @@ -1090,29 +1089,24 @@ impl Operation for ClearDecommission { mod pools_handler_tests { use super::{ AdminPoolStatus, Body, CancelDecommission, ClearDecommission, HeaderMap, ListPools, Method, Operation, - POOL_ACTIVATION_FLEET_PROOF_REQUIRED, Params, PoolAuditContext, S3ErrorCode, S3Request, StartDecommission, - StatusDecommission, StatusPool, Uri, contextualize_admin_pool_api_error, - decommission_admin_not_initialized_error_with_audit, decommission_peer_target, decommission_start_api_error, - decommission_start_rebalance_refresh_error, has_duplicate_indices, parse_mutation_pool_query, parse_pool_idx_by_id, - parse_status_pool_query, pool_admin_missing_credentials_error, pool_admin_missing_credentials_error_with_request, + POOL_ACTIVATION_FLEET_PROOF_REQUIRED, Params, PoolAuditContext, S3ErrorCode, StartDecommission, StatusDecommission, + StatusPool, Uri, contextualize_admin_pool_api_error, decommission_admin_not_initialized_error_with_audit, + decommission_peer_target, decommission_start_api_error, decommission_start_rebalance_refresh_error, + has_duplicate_indices, parse_mutation_pool_query, parse_pool_idx_by_id, parse_status_pool_query, + pool_admin_missing_credentials_error, pool_admin_missing_credentials_error_with_request, pool_admin_pool_index_error_with_audit, pool_admin_pool_not_found_error_with_audit, pool_admin_pool_parse_error_with_audit, pool_admin_query_parse_error, pool_admin_query_parse_error_with_audit, validate_pool_mutation_leader, validate_start_decommission_guards, }; + use crate::admin::operation::AdminRequest; use crate::admin::storage_api::runtime::{Endpoint, EndpointServerPools, Endpoints, PoolEndpoints}; - fn credential_less_request(method: Method, uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn credential_less_request(method: Method, uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(http::Extensions::new()) + .body(Body::empty()) + .build() } async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str, message: &str) { @@ -1582,7 +1576,7 @@ mod pools_handler_tests { fn assert_shared_gate_wiring(block: &str, item: &str, actions: &[&str], binds_credentials: bool) { assert_eq!( - block.matches("authorize_admin_request(").count(), + block.matches("authorize_admin_envelope(").count(), 1, "{item} must use exactly one shared gate" ); @@ -1595,14 +1589,14 @@ mod pools_handler_tests { assert!(block.contains(&format!("AdminAction::{action}")), "{item} must authorize with {action}"); } assert_eq!( - block.contains("let cred = authorize_admin_request("), + block.contains("let cred = authorize_admin_envelope("), binds_credentials, "{item} credential binding must match its payload-processing contract" ); } /// Pins the gate wiring itself: every pool handler authorizes through - /// `authorize_admin_request` with the same action vector it used before the + /// `authorize_admin_envelope` with the same action vector it used before the /// deduplication, and the mutating handlers keep deriving their audit actor /// from the caller-supplied access key (rustfs/backlog#1829). #[test] @@ -1629,7 +1623,7 @@ mod pools_handler_tests { for handler in ["StartDecommission", "CancelDecommission", "ClearDecommission"] { let block = source_block(production, &format!("impl Operation for {handler}")); assert!( - block.contains("let actor = MaskedAccessKey(&input_cred.access_key).to_string();"), + block.contains("let actor = MaskedAccessKey(input_cred.access_key()).to_string();"), "{handler} must keep masking the caller access key for its audit trail" ); } diff --git a/rustfs/src/admin/handlers/profile.rs b/rustfs/src/admin/handlers/profile.rs index a48b93f60..9d133b4d7 100644 --- a/rustfs/src/admin/handlers/profile.rs +++ b/rustfs/src/admin/handlers/profile.rs @@ -12,31 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::{ - auth::{authorize_admin_envelope, authorize_admin_request}, - router::Operation, -}; +use crate::admin::auth::authorize_admin_envelope; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; use crate::app::RequestEnvelope; use http::StatusCode; use matchit::Params; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Result, s3_error}; -use s3s::{Body, S3Request, S3Response}; +use rustfs_s3_types::{Body, S3Result, s3_error}; use tracing::info; /// The pre-check keeps these endpoints' historical `AccessDenied` missing-credentials /// response; the shared gate reports `InvalidRequest` "get cred failed". -pub(super) async fn authorize_profile_request(req: &S3Request) -> S3Result<()> { - if req.credentials.is_none() { - return Err(s3_error!(AccessDenied, "Signature is required")); - } - - authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ProfilingAdminAction)]).await?; - Ok(()) -} - -/// Owned-boundary form of [`authorize_profile_request`] with the same -/// `AccessDenied` pre-check. pub(super) async fn authorize_profile_envelope(envelope: &RequestEnvelope) -> S3Result<()> { if envelope.credentials().is_none() { return Err(s3_error!(AccessDenied, "Signature is required")); @@ -46,15 +32,15 @@ pub(super) async fn authorize_profile_envelope(envelope: &RequestEnvelope) -> S3 Ok(()) } -pub(super) fn profile_not_implemented_response(message: String) -> S3Response<(StatusCode, Body)> { - S3Response::new((StatusCode::NOT_IMPLEMENTED, Body::from(message))) +pub(super) fn profile_not_implemented_response(message: String) -> AdminResponse { + AdminResponse::new(StatusCode::NOT_IMPLEMENTED, Body::from(message)) } pub struct TriggerProfileCPU {} #[async_trait::async_trait] impl Operation for TriggerProfileCPU { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - authorize_profile_request(&req).await?; + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + authorize_profile_envelope(req.envelope()).await?; info!("Triggering CPU profile dump via S3 request..."); crate::profiling::log_cpu_pprof_dump_skipped(); @@ -65,8 +51,8 @@ impl Operation for TriggerProfileCPU { pub struct TriggerProfileMemory {} #[async_trait::async_trait] impl Operation for TriggerProfileMemory { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - authorize_profile_request(&req).await?; + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + authorize_profile_envelope(req.envelope()).await?; info!("Triggering Memory profile dump via S3 request..."); crate::profiling::log_memory_pprof_dump_skipped(); @@ -76,54 +62,27 @@ impl Operation for TriggerProfileMemory { #[cfg(test)] mod tests { - use super::{TriggerProfileCPU, TriggerProfileMemory, authorize_profile_envelope, authorize_profile_request}; + use super::{TriggerProfileCPU, TriggerProfileMemory, authorize_profile_envelope}; + use crate::admin::operation::test_support::TestRequest; + use crate::admin::operation::{AdminRequest, Operation}; + use crate::server::{PROFILE_CPU_PATH, PROFILE_MEMORY_PATH}; + use hyper::Method; + use matchit::Params; + use rustfs_s3_types::S3ErrorCode; #[tokio::test] async fn authorize_profile_envelope_keeps_the_access_denied_precheck() { - let legacy_request = s3s::S3Request { - input: s3s::Body::empty(), - method: http::Method::GET, - uri: http::Uri::from_static("/debug/pprof/cpu"), - headers: http::HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let legacy = authorize_profile_request(&legacy_request).await.expect_err("no credentials"); - - let owned_request = - crate::admin::operation::test_support::TestRequest::new(http::Method::GET, "/debug/pprof/cpu").build(); + let owned_request = TestRequest::new(Method::GET, "/debug/pprof/cpu").build(); let owned = authorize_profile_envelope(owned_request.envelope()) .await .expect_err("no credentials"); - assert_eq!(owned.code(), &rustfs_s3_types::S3ErrorCode::AccessDenied); - assert_eq!(owned.code(), legacy.code()); + assert_eq!(owned.code(), &S3ErrorCode::AccessDenied); assert_eq!(owned.message(), Some("Signature is required")); - assert_eq!(owned.message(), legacy.message()); } - use crate::admin::router::Operation; - use crate::server::{PROFILE_CPU_PATH, PROFILE_MEMORY_PATH}; - use http::{Extensions, HeaderMap, Uri}; - use hyper::Method; - use matchit::Params; - use rustfs_s3_types::S3ErrorCode; - use s3s::{Body, S3Request}; - fn build_profile_request(uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method: Method::GET, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn build_profile_request(uri: &'static str) -> AdminRequest { + TestRequest::new(Method::GET, uri).build() } #[tokio::test] diff --git a/rustfs/src/admin/handlers/quota.rs b/rustfs/src/admin/handlers/quota.rs index 34dc8150c..421d31677 100644 --- a/rustfs/src/admin/handlers/quota.rs +++ b/rustfs/src/admin/handlers/quota.rs @@ -16,9 +16,10 @@ use crate::admin::auth::{validate_admin_request, validate_admin_request_with_bucket}; use crate::admin::handlers::site_replication::site_replication_bucket_meta_hook; -use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::{ - AppContext, app_context_from_req, current_bucket_metadata_handle, current_notification_system_for_context, + AppContext, app_context_from_envelope, current_bucket_metadata_handle, current_notification_system_for_context, current_object_store_handle, }; use crate::admin::storage_api::bucket::metadata_sys::{self, BucketMetadataSys}; @@ -30,13 +31,11 @@ use crate::admin::storage_api::contract::bucket::{BucketOperations as _, BucketO use crate::auth::{check_key_valid, get_session_token}; use crate::error::ApiError; use crate::server::ADMIN_PREFIX; -use crate::server::RemoteAddr; use hyper::{Method, StatusCode}; use matchit::Params; use rustfs_madmin::{SITE_REPL_API_VERSION, SRBucketMeta}; use rustfs_policy::policy::action::{Action, AdminAction, S3Action}; -use rustfs_s3_types::{S3Error, S3Result, s3_error}; -use s3s::{Body, S3Request, S3Response}; +use rustfs_s3_types::{Body, S3Error, S3Result, s3_error}; use serde::{Deserialize, Serialize}; use serde_json; use std::sync::Arc; @@ -382,7 +381,7 @@ pub fn register_quota_route(r: &mut S3Router) -> std::io::Result #[async_trait::async_trait] impl Operation for SetBucketQuotaHandler { #[tracing::instrument(skip_all)] - async fn call(&self, mut req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result { warn!( event = EVENT_ADMIN_QUOTA_STATE, component = LOG_COMPONENT_ADMIN, @@ -392,16 +391,16 @@ impl Operation for SetBucketQuotaHandler { "admin quota state" ); - let Some(ref cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + let remote_addr = req.remote_addr(); validate_admin_request( - &req.headers, + req.headers(), &cred, owner, false, @@ -410,7 +409,7 @@ impl Operation for SetBucketQuotaHandler { ) .await?; - let bucket = bucket_from_params_or_query(¶ms, &req.uri); + let bucket = bucket_from_params_or_query(¶ms, req.uri()); if bucket.is_empty() { return Err(s3_error!(InvalidRequest, "bucket name is required")); } @@ -419,7 +418,7 @@ impl Operation for SetBucketQuotaHandler { .map_err(|e| s3_error!(InternalError, "failed to capture bucket incarnation: {}", e))?; let body = req - .input + .take_body() .store_all_limited(rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; @@ -472,7 +471,7 @@ impl Operation for SetBucketQuotaHandler { } .map_err(ApiError::from)?; - notify_quota_metadata_reload(app_context_from_req(&req), bucket.clone(), "set_bucket_quota").await; + notify_quota_metadata_reload(app_context_from_envelope(req.envelope()), bucket.clone(), "set_bucket_quota").await; if let Err(err) = site_replication_bucket_meta_hook(SRBucketMeta { bucket: bucket.clone(), @@ -501,7 +500,7 @@ impl Operation for SetBucketQuotaHandler { // Get real-time usage from data usage system let current_usage = current_usage_from_context(&bucket).await; - let json = if is_compat_set_bucket_quota_path(req.uri.path()) { + let json = if is_compat_set_bucket_quota_path(req.uri().path()) { String::new() } else { let response = BucketQuotaResponse { @@ -514,14 +513,14 @@ impl Operation for SetBucketQuotaHandler { serde_json::to_string(&response).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))? }; - Ok(S3Response::new((StatusCode::OK, Body::from(json)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json))) } } #[async_trait::async_trait] impl Operation for GetBucketQuotaHandler { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { warn!( event = EVENT_ADMIN_QUOTA_STATE, component = LOG_COMPONENT_ADMIN, @@ -531,20 +530,20 @@ impl Operation for GetBucketQuotaHandler { "admin quota state" ); - let Some(ref cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let bucket = bucket_from_params_or_query(¶ms, &req.uri); + let bucket = bucket_from_params_or_query(¶ms, req.uri()); if bucket.is_empty() { return Err(s3_error!(InvalidRequest, "bucket name is required")); } - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + let remote_addr = req.remote_addr(); validate_admin_request_with_bucket( - &req.headers, + req.headers(), &cred, owner, false, @@ -559,7 +558,7 @@ impl Operation for GetBucketQuotaHandler { let quota_checker = QuotaChecker::new(metadata_sys_lock.clone()); - if is_compat_get_bucket_quota_path(req.uri.path()) { + if is_compat_get_bucket_quota_path(req.uri().path()) { if !quota_checker.bucket_exists(&bucket).await { return Err(s3_error!(NoSuchBucket, "bucket not found: {}", bucket)); } @@ -569,7 +568,7 @@ impl Operation for GetBucketQuotaHandler { .map_err(|e| s3_error!(InternalError, "failed to get quota: {}", e))?; let json = serde_json::to_string(&compat_bucket_quota_response("a)) .map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; - return Ok(S3Response::new((StatusCode::OK, Body::from(json)))); + return Ok(AdminResponse::new(StatusCode::OK, Body::from(json))); } let (quota, current_usage) = quota_checker @@ -587,14 +586,14 @@ impl Operation for GetBucketQuotaHandler { let json = serde_json::to_string(&response).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; - Ok(S3Response::new((StatusCode::OK, Body::from(json)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json))) } } #[async_trait::async_trait] impl Operation for ClearBucketQuotaHandler { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { warn!( event = EVENT_ADMIN_QUOTA_STATE, component = LOG_COMPONENT_ADMIN, @@ -604,16 +603,16 @@ impl Operation for ClearBucketQuotaHandler { "admin quota state" ); - let Some(ref cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + let remote_addr = req.remote_addr(); validate_admin_request( - &req.headers, + req.headers(), &cred, owner, false, @@ -652,7 +651,7 @@ impl Operation for ClearBucketQuotaHandler { .await .map_err(|e| s3_error!(InternalError, "failed to clear quota: {}", e))?; - notify_quota_metadata_reload(app_context_from_req(&req), bucket.clone(), "clear_bucket_quota").await; + notify_quota_metadata_reload(app_context_from_envelope(req.envelope()), bucket.clone(), "clear_bucket_quota").await; if let Err(err) = site_replication_bucket_meta_hook(SRBucketMeta { bucket: bucket.clone(), @@ -701,14 +700,14 @@ impl Operation for ClearBucketQuotaHandler { let json = serde_json::to_string(&response).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; - Ok(S3Response::new((StatusCode::OK, Body::from(json)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json))) } } #[async_trait::async_trait] impl Operation for GetBucketQuotaStatsHandler { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { warn!( event = EVENT_ADMIN_QUOTA_STATE, component = LOG_COMPONENT_ADMIN, @@ -718,21 +717,21 @@ impl Operation for GetBucketQuotaStatsHandler { "admin quota state" ); - let Some(ref cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; let bucket = params.get("bucket").unwrap_or("").to_string(); if bucket.is_empty() { return Err(s3_error!(InvalidRequest, "bucket name is required")); } - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + let remote_addr = req.remote_addr(); validate_admin_request_with_bucket( - &req.headers, + req.headers(), &cred, owner, false, @@ -770,14 +769,14 @@ impl Operation for GetBucketQuotaStatsHandler { let json = serde_json::to_string(&response).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; - Ok(S3Response::new((StatusCode::OK, Body::from(json)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json))) } } #[async_trait::async_trait] impl Operation for CheckBucketQuotaHandler { #[tracing::instrument(skip_all)] - async fn call(&self, mut req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result { warn!( event = EVENT_ADMIN_QUOTA_STATE, component = LOG_COMPONENT_ADMIN, @@ -787,21 +786,21 @@ impl Operation for CheckBucketQuotaHandler { "admin quota state" ); - let Some(ref cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; let bucket = params.get("bucket").unwrap_or("").to_string(); if bucket.is_empty() { return Err(s3_error!(InvalidRequest, "bucket name is required")); } - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + let remote_addr = req.remote_addr(); validate_admin_request_with_bucket( - &req.headers, + req.headers(), &cred, owner, false, @@ -812,7 +811,7 @@ impl Operation for CheckBucketQuotaHandler { .await?; let body = req - .input + .take_body() .store_all_limited(rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE) .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; @@ -866,7 +865,7 @@ impl Operation for CheckBucketQuotaHandler { let json = serde_json::to_string(&response).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; - Ok(S3Response::new((StatusCode::OK, Body::from(json)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json))) } } @@ -877,7 +876,7 @@ impl Operation for CheckBucketQuotaHandler { #[async_trait::async_trait] impl Operation for MigrateBucketQuotaProtocolHandler { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { warn!( event = EVENT_ADMIN_QUOTA_STATE, component = LOG_COMPONENT_ADMIN, @@ -887,16 +886,16 @@ impl Operation for MigrateBucketQuotaProtocolHandler { "admin quota state" ); - let Some(ref cred) = req.credentials else { + let Some(cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), cred.access_key()).await?; - let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); + let remote_addr = req.remote_addr(); validate_admin_request( - &req.headers, + req.headers(), &cred, owner, false, @@ -937,7 +936,7 @@ impl Operation for MigrateBucketQuotaProtocolHandler { let json = serde_json::to_string(&report).map_err(|e| s3_error!(InternalError, "failed to serialize response: {}", e))?; - Ok(S3Response::new((StatusCode::OK, Body::from(json)))) + Ok(AdminResponse::new(StatusCode::OK, Body::from(json))) } } diff --git a/rustfs/src/admin/handlers/realtime.rs b/rustfs/src/admin/handlers/realtime.rs index ea3392a77..7226051dd 100644 --- a/rustfs/src/admin/handlers/realtime.rs +++ b/rustfs/src/admin/handlers/realtime.rs @@ -18,26 +18,21 @@ //! keeping the response format explicitly NDJSON. It is not a Prometheus text //! exposition endpoint. -use crate::admin::auth::authorize_admin_request; -use crate::admin::router::Operation; +use crate::admin::auth::authorize_admin_envelope; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; use crate::admin::storage_api::access::spawn_traced; use crate::admin::storage_api::metrics::{CollectMetricsOpts, MetricType, collect_local_metrics}; use bytes::Bytes; -use futures::{Stream, StreamExt}; +use http::header::CONTENT_TYPE; use http::{HeaderMap, HeaderValue, Uri}; use hyper::StatusCode; use matchit::Params; use rustfs_madmin::metrics::RealtimeMetrics; use rustfs_madmin::utils::parse_duration; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Result, s3_error}; -use s3s::header::CONTENT_TYPE; -use s3s::stream::{ByteStream, DynByteStream}; -use s3s::{Body, S3Request, S3Response, StdError}; +use rustfs_s3_types::{Body, S3Result, StdError, s3_error}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; -use std::pin::Pin; -use std::task::{Context, Poll}; use std::time::Duration as StdDuration; use tokio::select; use tokio::sync::mpsc; @@ -164,42 +159,27 @@ fn resolve_sample_count(mp: &MetricsParams) -> u64 { requested } -struct MetricsStream { - inner: ReceiverStream>, -} - -impl Stream for MetricsStream { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = Pin::into_inner(self); - this.inner.poll_next_unpin(cx) - } -} - -impl ByteStream for MetricsStream {} - pub struct MetricsHandler {} /// The pre-check keeps this endpoint's historical `AccessDenied` missing-credentials /// response; the shared gate reports `InvalidRequest` "get cred failed". -async fn authorize_metrics_request(req: &S3Request) -> S3Result<()> { - if req.credentials.is_none() { +async fn authorize_metrics_request(req: &AdminRequest) -> S3Result<()> { + if req.credentials().is_none() { return Err(s3_error!(AccessDenied, "Signature is required")); } - authorize_admin_request(req, vec![Action::AdminAction(AdminAction::GetMetricsAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::GetMetricsAction)]).await?; Ok(()) } #[async_trait::async_trait] impl Operation for MetricsHandler { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { - debug!("handle MetricsHandler, uri: {:?}, params: {:?}", req.uri, params); + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { + debug!("handle MetricsHandler, uri: {:?}, params: {:?}", req.uri(), params); authorize_metrics_request(&req).await?; debug!("validated console metrics admin authorization"); - let mp = extract_metrics_init_params(&req.uri); + let mp = extract_metrics_init_params(req.uri()); debug!("mp: {:?}", mp); let tick = parse_duration(&mp.tick).unwrap_or_else(|_| StdDuration::from_secs(3)); @@ -224,11 +204,8 @@ impl Operation for MetricsHandler { job_id: mp.by_job_id, dep_id: mp.by_dep_id, }; - let (tx, rx) = mpsc::channel(10); - let in_stream: DynByteStream = Box::pin(MetricsStream { - inner: ReceiverStream::new(rx), - }); - let body = Body::from(in_stream); + let (tx, rx) = mpsc::channel::>(10); + let body = Body::from_stream(ReceiverStream::new(rx)); spawn_traced(async move { while n > 0 { @@ -271,7 +248,7 @@ impl Operation for MetricsHandler { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static(CONSOLE_METRICS_CONTENT_TYPE)); - Ok(S3Response::with_headers((StatusCode::OK, body), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, body, header)) } } @@ -281,25 +258,20 @@ mod tests { CONSOLE_METRICS_CONTENT_TYPE, DEFAULT_METRICS_SAMPLES, MAX_METRICS_SAMPLES, MetricsHandler, extract_metrics_init_params, resolve_sample_count, }; - use crate::admin::router::Operation; + use crate::admin::operation::AdminRequest; + use crate::admin::operation::Operation; use http::{Extensions, HeaderMap, Uri}; use hyper::Method; use matchit::Params; + use rustfs_s3_types::Body; use rustfs_s3_types::S3ErrorCode; - use s3s::{Body, S3Request}; - fn build_metrics_request(uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method: Method::GET, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn build_metrics_request(uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(Method::GET, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(Extensions::new()) + .body(Body::empty()) + .build() } #[test] diff --git a/rustfs/src/admin/handlers/rebalance.rs b/rustfs/src/admin/handlers/rebalance.rs index 998089f73..086769b3b 100644 --- a/rustfs/src/admin/handlers/rebalance.rs +++ b/rustfs/src/admin/handlers/rebalance.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::admin::runtime_sources::object_store_from_extensions; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::runtime_sources::object_store_from_envelope; use crate::admin::storage_api::contract::admin::StorageAdminApi; use crate::admin::storage_api::contract::bucket::{BucketOperations, BucketOptions}; use crate::admin::storage_api::error::StorageError; @@ -23,24 +24,21 @@ use crate::admin::storage_api::runtime::{ECStore, NotificationSys}; use crate::{ admin::runtime_sources::current_notification_system, admin::{ - auth::authorize_admin_request, - router::{AdminOperation, Operation, S3Router}, + auth::authorize_admin_envelope, + router::{AdminOperation, S3Router}, }, - server::{ADMIN_PREFIX, RemoteAddr}, + server::ADMIN_PREFIX, }; +use http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use http::{HeaderMap, HeaderValue, StatusCode, Uri}; use hyper::Method; use matchit::Params; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; +use rustfs_s3_types::{Body, S3Error, S3ErrorCode, S3Result, s3_error}; use rustfs_utils::{ MaskedAccessKey, http::{AMZ_REQUEST_ID, REQUEST_ID_HEADER}, }; -use s3s::{ - Body, S3Request, S3Response, - header::{CONTENT_LENGTH, CONTENT_TYPE}, -}; use serde::{Deserialize, Serialize}; use std::{future::Future, sync::Arc, time::Duration}; use time::OffsetDateTime; @@ -63,10 +61,8 @@ fn admin_request_id(headers: &HeaderMap) -> Option<&str> { .and_then(|value| value.to_str().ok()) } -fn admin_remote_addr(req: &S3Request) -> Option { - req.extensions - .get::>() - .and_then(|opt| opt.map(|addr| addr.0.to_string())) +fn admin_remote_addr(req: &AdminRequest) -> Option { + req.remote_addr().map(|addr| addr.to_string()) } fn log_rebalance_request_rejected(action: &str, reason: &str, request_id: &str, actor: &str, remote_addr: &str) { @@ -564,8 +560,8 @@ pub struct RebalanceStart {} #[async_trait::async_trait] impl Operation for RebalanceStart { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string(); + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let request_id = admin_request_id(req.headers()).unwrap_or_default().to_string(); let remote_addr = admin_remote_addr(&req).unwrap_or_default(); info!( event = EVENT_ADMIN_REBALANCE_STATE, @@ -578,19 +574,19 @@ impl Operation for RebalanceStart { "admin rebalance state" ); - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; - let actor = MaskedAccessKey(&input_cred.access_key).to_string(); + let actor = MaskedAccessKey(input_cred.access_key()).to_string(); - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::RebalanceAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::RebalanceAdminAction)]).await?; - if rebalance_query_present(&req.uri) { + if rebalance_query_present(req.uri()) { log_rebalance_request_rejected("start", "invalid_query_parameters", &request_id, &actor, &remote_addr); return Err(s3_error!(InvalidArgument, "rebalance start does not accept query parameters")); } - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object layer is not initialized")); }; @@ -870,7 +866,7 @@ impl Operation for RebalanceStart { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -880,8 +876,8 @@ pub struct RebalanceStatus {} #[async_trait::async_trait] impl Operation for RebalanceStatus { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string(); + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let request_id = admin_request_id(req.headers()).unwrap_or_default().to_string(); let remote_addr = admin_remote_addr(&req).unwrap_or_default(); info!( event = EVENT_ADMIN_REBALANCE_STATE, @@ -894,14 +890,14 @@ impl Operation for RebalanceStatus { "admin rebalance state" ); - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; - let actor = MaskedAccessKey(&input_cred.access_key).to_string(); + let actor = MaskedAccessKey(input_cred.access_key()).to_string(); - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::RebalanceAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::RebalanceAdminAction)]).await?; - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object layer is not initialized")); }; @@ -933,9 +929,7 @@ impl Operation for RebalanceStatus { let (parts, body) = s3s::S3Error::with_message(s3s::S3ErrorCode::NoSuchResource, REBALANCE_NOT_STARTED_MESSAGE) .to_http_response()? .into_parts(); - let mut response = S3Response::with_headers((parts.status, body), parts.headers); - response.extensions = parts.extensions; - return Ok(response); + return Ok(AdminResponse::with_headers(parts.status, body.into(), parts.headers)); } return Err(s3_error!(InternalError, "failed to load rebalance metadata from pool 0: {}", err)); @@ -975,7 +969,7 @@ impl Operation for RebalanceStatus { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -1029,8 +1023,8 @@ pub struct RebalanceStop {} #[async_trait::async_trait] impl Operation for RebalanceStop { #[tracing::instrument(skip_all)] - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string(); + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let request_id = admin_request_id(req.headers()).unwrap_or_default().to_string(); let remote_addr = admin_remote_addr(&req).unwrap_or_default(); info!( event = EVENT_ADMIN_REBALANCE_STATE, @@ -1043,19 +1037,19 @@ impl Operation for RebalanceStop { "admin rebalance state" ); - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; - let actor = MaskedAccessKey(&input_cred.access_key).to_string(); + let actor = MaskedAccessKey(input_cred.access_key()).to_string(); - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::RebalanceAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::RebalanceAdminAction)]).await?; - if rebalance_query_present(&req.uri) { + if rebalance_query_present(req.uri()) { log_rebalance_request_rejected("stop", "invalid_query_parameters", &request_id, &actor, &remote_addr); return Err(s3_error!(InvalidArgument, "rebalance stop does not accept query parameters")); } - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object layer is not initialized")); }; @@ -1159,7 +1153,7 @@ impl Operation for RebalanceStop { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); header.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } @@ -1201,13 +1195,14 @@ mod rebalance_handler_tests { use super::calculate_rebalance_progress; use super::{ Body, HeaderMap, Method, Operation, Params, RebalPoolProgress, RebalanceAdminStatus, RebalancePoolStatus, RebalanceStart, - RebalanceStartStep, RebalanceStatus, RebalanceStop, RebalanceStopPropagationStatus, S3ErrorCode, S3Request, StatusCode, - Uri, build_rebalance_admin_status, build_rebalance_pool_statuses, build_rebalance_stop_propagation_status, + RebalanceStartStep, RebalanceStatus, RebalanceStop, RebalanceStopPropagationStatus, S3ErrorCode, StatusCode, Uri, + build_rebalance_admin_status, build_rebalance_pool_statuses, build_rebalance_stop_propagation_status, is_rebalance_start_fleet_proof_retryable, rebalance_init_failure_error, rebalance_pool_used, rebalance_query_present, rebalance_remaining_buckets, rebalance_rollback_failure_message, rebalance_rollback_stop_failure_message, rebalance_start_failure_error, rebalance_start_rollback_error, rebalance_start_steps, rebalance_stop_target_id, rebalance_used_pct, retry_rebalance_start_operation, rollback_result_label, stop_rebalance_admission_first, }; + use crate::admin::operation::AdminRequest; use crate::admin::storage_api::error::StorageError; use crate::admin::storage_api::rebalance::{ DiskStat, RebalSaveOpt, RebalStatus, RebalanceCleanupWarningEntry, RebalanceCleanupWarnings, RebalanceInfo, @@ -1215,18 +1210,12 @@ mod rebalance_handler_tests { }; use time::OffsetDateTime; - fn credential_less_request(method: Method, uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn credential_less_request(method: Method, uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(http::Extensions::new()) + .body(Body::empty()) + .build() } async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str) { @@ -2309,7 +2298,7 @@ mod rebalance_handler_tests { for handler in ["RebalanceStart", "RebalanceStatus", "RebalanceStop"] { let block = source_block(production, &format!("impl Operation for {handler}")); assert_eq!( - block.matches("authorize_admin_request(").count(), + block.matches("authorize_admin_envelope(").count(), 1, "{handler} must use exactly one shared gate" ); @@ -2323,11 +2312,11 @@ mod rebalance_handler_tests { "{handler} must authorize with RebalanceAdminAction" ); assert!( - !block.contains("let cred = authorize_admin_request("), + !block.contains("let cred = authorize_admin_envelope("), "{handler} does not consume the authenticated credentials" ); assert!( - block.contains("let actor = MaskedAccessKey(&input_cred.access_key).to_string();"), + block.contains("let actor = MaskedAccessKey(input_cred.access_key()).to_string();"), "{handler} must keep masking the caller access key for its audit trail" ); } diff --git a/rustfs/src/admin/handlers/service_account.rs b/rustfs/src/admin/handlers/service_account.rs index 2b7721278..37dff4ef3 100644 --- a/rustfs/src/admin/handlers/service_account.rs +++ b/rustfs/src/admin/handlers/service_account.rs @@ -15,15 +15,18 @@ use super::iam_error::iam_error_to_s3_error; use crate::admin::access_key_identity; use crate::admin::handlers::site_replication::{encode_service_account_replication_policy, site_replication_iam_change_hook}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; use crate::admin::runtime_sources::current_action_credentials; use crate::admin::utils::{encode_compatible_admin_payload, has_space_be, is_compat_admin_request, read_compatible_admin_body}; use crate::auth::{constant_time_eq, get_condition_values, get_session_token}; -use crate::server::{ADMIN_PREFIX, RemoteAddr}; +use crate::server::ADMIN_PREFIX; use crate::{ - admin::router::{AdminOperation, Operation, S3Router}, + admin::router::{AdminOperation, S3Router}, auth::check_key_valid, }; use http::HeaderMap; +use http::header::CONTENT_LENGTH; +use http::header::CONTENT_TYPE; use hyper::{Method, StatusCode}; use matchit::Params; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; @@ -39,10 +42,8 @@ use rustfs_madmin::{ }; use rustfs_policy::policy::action::{Action, AdminAction}; use rustfs_policy::policy::{Args, DEFAULT_VERSION, Policy}; -use rustfs_s3_types::S3ErrorCode::InvalidRequest; +use rustfs_s3_types::{Body, S3ErrorCode::InvalidRequest}; use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; -use s3s::header::CONTENT_LENGTH; -use s3s::{Body, S3Request, S3Response, header::CONTENT_TYPE}; use serde::Deserialize; use serde_urlencoded::from_bytes; use std::collections::HashMap; @@ -315,15 +316,16 @@ pub fn register_service_account_route(r: &mut S3Router) -> std:: pub struct AddServiceAccount {} #[async_trait::async_trait] impl Operation for AddServiceAccount { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(req_cred) = req.credentials else { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(req_cred) = req.credentials() 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(), &req_cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), req_cred.access_key()).await?; - let body = read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), &cred.secret_key).await?; + let body = + read_compatible_admin_body(req.take_body(), MAX_ADMIN_REQUEST_BODY_SIZE, req.uri().path(), &cred.secret_key).await?; let create_req: AddServiceAccountReq = serde_json::from_slice(&body[..]).map_err(|e| s3_error!(InvalidRequest, "unmarshal body failed, e: {:?}", e))?; @@ -384,13 +386,7 @@ impl Operation for AddServiceAccount { groups: &cred.groups, action: Action::AdminAction(AdminAction::CreateServiceAccountAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -525,12 +521,12 @@ impl Operation for AddServiceAccount { }; let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?; - let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?; + let (body, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, body)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(body), header)) } } @@ -579,9 +575,9 @@ async fn build_info_service_account_resp( pub struct UpdateServiceAccount {} #[async_trait::async_trait] impl Operation for UpdateServiceAccount { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AccessKeyQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?; input @@ -596,7 +592,8 @@ impl Operation for UpdateServiceAccount { let access_key = query.access_key; - let Some(input_cred) = req.credentials else { + let input = req.take_body(); + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "get cred failed")); }; @@ -610,8 +607,7 @@ impl Operation for UpdateServiceAccount { // })?; let body = - read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), input_cred.secret_key.expose()) - .await?; + read_compatible_admin_body(input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri().path(), input_cred.secret_key()).await?; let update_req: UpdateServiceAccountReq = serde_json::from_slice(&body[..]).map_err(|e| s3_error!(InvalidRequest, "unmarshal body failed, e: {:?}", e))?; @@ -619,7 +615,7 @@ impl Operation for UpdateServiceAccount { update_req.validate().map_err(|e| S3Error::with_message(InvalidRequest, e))?; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; if !iam_store .is_allowed(&Args { @@ -627,13 +623,7 @@ impl Operation for UpdateServiceAccount { groups: &cred.groups, action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -696,16 +686,16 @@ impl Operation for UpdateServiceAccount { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::NO_CONTENT, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::NO_CONTENT, Body::empty(), header)) } } pub struct InfoServiceAccount {} #[async_trait::async_trait] impl Operation for InfoServiceAccount { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AccessKeyQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?; input @@ -729,12 +719,12 @@ impl Operation for InfoServiceAccount { .await .map_err(|e| map_service_account_lookup_error(e, "get service account failed"))?; - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() 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?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; if !iam_store .is_allowed(&Args { @@ -742,13 +732,7 @@ impl Operation for InfoServiceAccount { groups: &cred.groups, action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -763,21 +747,21 @@ impl Operation for InfoServiceAccount { let resp = build_info_service_account_resp(&iam_store, &svc_account, session_policy).await?; let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?; - let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?; + let (body, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, body)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(body), header)) } } pub struct TemporaryAccountInfo {} #[async_trait::async_trait] impl Operation for TemporaryAccountInfo { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AccessKeyQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?; input @@ -790,12 +774,12 @@ impl Operation for TemporaryAccountInfo { return Err(s3_error!(InvalidArgument, "access key is empty")); } - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() 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?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else { return Err(s3_error!(InvalidRequest, "iam not init")); @@ -807,13 +791,7 @@ impl Operation for TemporaryAccountInfo { groups: &cred.groups, action: Action::AdminAction(AdminAction::ListTemporaryAccountsAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -831,21 +809,21 @@ impl Operation for TemporaryAccountInfo { let resp: TemporaryAccountInfoResp = build_info_service_account_resp(&iam_store, &temp_account, session_policy).await?; let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?; - let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?; + let (body, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, body)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(body), header)) } } pub struct InfoAccessKey {} #[async_trait::async_trait] impl Operation for InfoAccessKey { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AccessKeyQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?; input @@ -854,12 +832,12 @@ impl Operation for InfoAccessKey { } }; - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() 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?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; let access_key = if query.access_key.is_empty() { cred.access_key.clone() @@ -873,13 +851,7 @@ impl Operation for InfoAccessKey { let target_cred = iam_store.get_user(&access_key).await.map(|identity| identity.credentials); - let conditions = get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ); + let conditions = get_condition_values(req.headers(), &cred, None, None, req.remote_addr()); let claims = cred.claims_or_empty(); if !iam_store @@ -929,12 +901,12 @@ impl Operation for InfoAccessKey { let resp = access_key_identity::resolve_info_access_key_resp(&iam_store, access_key, target_cred).await?; let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?; - let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?; + let (body, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, body)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(body), header)) } } @@ -946,9 +918,9 @@ pub struct ListServiceAccountQuery { pub struct ListServiceAccount {} #[async_trait::async_trait] impl Operation for ListServiceAccount { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: ListServiceAccountQuery = from_bytes(query.as_bytes()) .map_err(|_e| s3_error!(InvalidArgument, "invalid service account query parameters"))?; input @@ -957,12 +929,12 @@ impl Operation for ListServiceAccount { } }; - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() 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) + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()) .await .map_err(|e| { debug!( @@ -1003,13 +975,7 @@ impl Operation for ListServiceAccount { groups: &cred.groups, action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -1049,18 +1015,18 @@ impl Operation for ListServiceAccount { access_key: sa.access_key, name: sa.name, description: sa.description, - expiration: expiration_for_admin_path(req.uri.path(), sa.expiration), + expiration: expiration_for_admin_path(req.uri().path(), sa.expiration), }) .collect(); let data = serde_json::to_vec(&ListServiceAccountsResp { accounts }) .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {e}")))?; - let (data, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, data)?; + let (data, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, data)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -1113,19 +1079,19 @@ fn parse_list_access_keys_query(query: Option<&str>) -> ListAccessKeysQuery { pub struct ListAccessKeysBulk {} #[async_trait::async_trait] impl Operation for ListAccessKeysBulk { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let query = { parse_list_access_keys_query(req.uri.query()) }; + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let query = { parse_list_access_keys_query(req.uri().query()) }; if query.all && !query.users.is_empty() { return Err(s3_error!(InvalidRequest, "either specify users or all, not both")); } - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() 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?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else { return Err(s3_error!(InvalidRequest, "iam not init")); @@ -1147,13 +1113,7 @@ impl Operation for ListAccessKeysBulk { groups: &cred.groups, action: Action::AdminAction(AdminAction::ListUsersAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -1170,13 +1130,7 @@ impl Operation for ListAccessKeysBulk { groups: &cred.groups, action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -1240,7 +1194,7 @@ impl Operation for ListAccessKeysBulk { .map(|sts| { access_key_identity::list_entry_from_credentials( &sts, - expiration_for_admin_path(req.uri.path(), sts.expiration), + expiration_for_admin_path(req.uri().path(), sts.expiration), ) }) .collect(); @@ -1269,7 +1223,7 @@ impl Operation for ListAccessKeysBulk { .map(|svc| { access_key_identity::list_entry_from_credentials( &svc, - expiration_for_admin_path(req.uri.path(), svc.expiration), + expiration_for_admin_path(req.uri().path(), svc.expiration), ) }) .collect(); @@ -1284,25 +1238,25 @@ impl Operation for ListAccessKeysBulk { let data = serde_json::to_vec(&access_key_map) .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal access keys err {e}")))?; - let (data, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, data)?; + let (data, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, data)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } pub struct DeleteServiceAccount {} #[async_trait::async_trait] impl Operation for DeleteServiceAccount { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(input_cred) = req.credentials else { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(input_cred) = req.credentials() 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) + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()) .await .map_err(|e| { debug!( @@ -1317,7 +1271,7 @@ impl Operation for DeleteServiceAccount { })?; let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AccessKeyQuery = from_bytes(query.as_bytes()) .map_err(|_e| s3_error!(InvalidArgument, "invalid access key query parameters"))?; input @@ -1351,13 +1305,7 @@ impl Operation for DeleteServiceAccount { groups: &cred.groups, action: Action::AdminAction(AdminAction::RemoveServiceAccountAdminAction), bucket: "", - conditions: &get_condition_values( - &req.headers, - &cred, - None, - None, - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), - ), + conditions: &get_condition_values(req.headers(), &cred, None, None, req.remote_addr()), is_owner: owner, object: "", claims: cred.claims_or_empty(), @@ -1431,8 +1379,9 @@ impl Operation for DeleteServiceAccount { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers( - (delete_service_account_success_status(req.uri.path()), Body::empty()), + Ok(AdminResponse::with_headers( + delete_service_account_success_status(req.uri().path()), + Body::empty(), header, )) } diff --git a/rustfs/src/admin/handlers/system.rs b/rustfs/src/admin/handlers/system.rs index 912d9ef9e..29308e43c 100644 --- a/rustfs/src/admin/handlers/system.rs +++ b/rustfs/src/admin/handlers/system.rs @@ -13,15 +13,16 @@ // limitations under the License. use super::{cluster_snapshot, realtime}; -use crate::admin::auth::authorize_admin_request; +use crate::admin::auth::authorize_admin_envelope; use crate::admin::handlers::account::{ACCOUNT_INFO_ROUTE, ACCOUNT_PASSWORD_ROUTE}; use crate::admin::handlers::mfa::{ACCOUNT_MFA_ROUTE, MFA_CHALLENGE_ROUTE, USER_MFA_ROUTE}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; use crate::admin::route_policy::{ ADMIN_ROUTE_POLICY_SPECS, DEFERRED_ADMIN_ROUTE_POLICIES, DeferredAdminRoutePolicy, DeferredRoutePolicyReason, }; -use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::router::{AdminOperation, S3Router}; use crate::admin::runtime_sources::{ - DefaultAdminUsecase, QueryServerInfoRequest, current_endpoints_handle, default_admin_usecase, object_store_from_req, + DefaultAdminUsecase, QueryServerInfoRequest, current_endpoints_handle, default_admin_usecase, object_store_from_envelope, }; use crate::admin::storage_api::bucket::replication::{ REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION, REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS, REMOTE_TARGET_UNSUPPORTED_FIELDS, @@ -36,16 +37,15 @@ use crate::admin::utils::is_compat_admin_request; use crate::runtime_capabilities::{EndpointTopologySnapshotProvider, RustFsObservabilitySnapshotProvider}; use crate::server::ADMIN_PREFIX; use crate::workload_admission::workload_admission_registry_snapshot; +use http::header::CONTENT_TYPE; use http::{HeaderMap, HeaderValue}; use hyper::{Method, StatusCode}; use matchit::Params; use rustfs_concurrency::WorkloadAdmissionRegistrySnapshot; use rustfs_madmin::{InfoMessage, StorageInfo}; use rustfs_policy::policy::action::{Action, AdminAction, S3Action}; -use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; +use rustfs_s3_types::{Body, S3Error, S3ErrorCode, S3Result, s3_error}; use rustfs_security_governance::{AdminRouteSpec, HttpMethod}; -use s3s::header::CONTENT_TYPE; -use s3s::{Body, S3Request, S3Response}; use serde::{Deserialize, Serialize}; use std::sync::atomic::{AtomicBool, Ordering}; use tracing::{error, info, warn}; @@ -250,13 +250,13 @@ fn request_graceful_shutdown() {} #[async_trait::async_trait] impl Operation for ServiceHandle { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { log_system_request_rejected!("service_handle", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - let Some(action) = service_action_from_uri(&req.uri) else { + let Some(action) = service_action_from_uri(req.uri()) else { log_system_request_rejected!("service_handle", "invalid_action"); return Err(s3_error!(InvalidRequest, "action must be one of: restart, stop, freeze, unfreeze")); }; @@ -268,7 +268,7 @@ impl Operation for ServiceHandle { ServiceAction::Freeze | ServiceAction::Unfreeze => AdminAction::ServiceFreezeAdminAction, }; - authorize_admin_request(&req, vec![Action::AdminAction(admin_action)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(admin_action)]).await?; let response = match action { ServiceAction::Restart => { @@ -344,7 +344,7 @@ impl Operation for ServiceHandle { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_system_response_emitted!("service_handle"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -361,12 +361,12 @@ struct ServerUpdateStatus { #[async_trait::async_trait] impl Operation for UpdateHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { log_system_request_rejected!("server_update", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ServerUpdateAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ServerUpdateAdminAction)]).await?; // MinIO's server-update downloads and swaps the binary in place. RustFS // intentionally does not implement in-process self-update: binaries are @@ -394,7 +394,7 @@ impl Operation for UpdateHandler { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_system_response_emitted!("server_update"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -468,13 +468,13 @@ fn bitrot_selftest_status_str() -> &'static str { #[async_trait::async_trait] impl Operation for ServerInfoHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { log_system_request_rejected!("query_server_info", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?; let usecase = default_admin_usecase(); let info = usecase @@ -488,7 +488,7 @@ impl Operation for ServerInfoHandler { bitrot_selftest: bitrot_selftest_status_str(), }; - let data = encode_server_info_response(req.uri.path(), &response).map_err(|e| { + let data = encode_server_info_response(req.uri().path(), &response).map_err(|e| { log_system_request_failed!("query_server_info", "serialize_server_info_failed", e); S3Error::with_message(S3ErrorCode::InternalError, "parse serverInfo failed") })?; @@ -497,7 +497,7 @@ impl Operation for ServerInfoHandler { header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_system_response_emitted!("query_server_info"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -529,16 +529,16 @@ fn inspect_data_target(uri: &http::Uri) -> Option<(String, String)> { #[async_trait::async_trait] impl Operation for InspectDataHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { use crate::admin::storage_api::contract::object::ObjectIO as _; use crate::admin::storage_api::object::StorageObjectOptions; use tokio::io::AsyncReadExt; - if req.credentials.is_none() { + if req.credentials().is_none() { log_system_request_rejected!("inspect_data", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::InspectDataAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::InspectDataAction)]).await?; // MinIO's inspect-data exports a signed archive of raw drive files for a // `volume`/`file` glob. RustFS erasure-codes and (optionally) encrypts @@ -546,14 +546,14 @@ impl Operation for InspectDataHandler { // back; instead we return the reconstructed raw object bytes for the // requested `volume` (bucket) + `file` (object), which is the honest, // usable form of "inspect this object's data" against an EC store. - let Some((bucket, object)) = inspect_data_target(&req.uri) else { + let Some((bucket, object)) = inspect_data_target(req.uri()) else { return Err(s3_error!( InvalidRequest, "inspect-data requires `volume` (bucket) and `file` (object) query parameters" )); }; - let Some(store) = object_store_from_req(&req) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { log_system_request_failed!("inspect_data", "object_store_unavailable", "not initialized"); return Err(S3Error::with_message(S3ErrorCode::InternalError, "object store is not initialized")); }; @@ -586,7 +586,7 @@ impl Operation for InspectDataHandler { header.insert(http::header::CONTENT_DISPOSITION, disposition); } log_system_response_emitted!("inspect_data"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(buf)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(buf), header)) } } @@ -594,13 +594,13 @@ pub struct StorageInfoHandler {} #[async_trait::async_trait] impl Operation for StorageInfoHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { log_system_request_rejected!("query_storage_info", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::StorageInfoAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::StorageInfoAdminAction)]).await?; let usecase = default_admin_usecase(); let info = usecase.execute_query_storage_info().await.map_err(S3Error::from)?; @@ -609,7 +609,7 @@ impl Operation for StorageInfoHandler { admin_discovery: system_admin_discovery(&usecase), }; - let data = encode_storage_info_response(req.uri.path(), &response).map_err(|e| { + let data = encode_storage_info_response(req.uri().path(), &response).map_err(|e| { log_system_request_failed!("query_storage_info", "serialize_storage_info_failed", e); S3Error::with_message(S3ErrorCode::InternalError, "failed to serialize storage info") })?; @@ -618,7 +618,7 @@ impl Operation for StorageInfoHandler { header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_system_response_emitted!("query_storage_info"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -1179,13 +1179,13 @@ fn summarize_named_capability_statuses( #[async_trait::async_trait] impl Operation for RuntimeCapabilitiesHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { log_system_request_rejected!("runtime_capabilities", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, runtime_capabilities_gate_actions()).await?; + authorize_admin_envelope(req.envelope(), runtime_capabilities_gate_actions()).await?; let response = build_runtime_capabilities_response().await.map_err(|err| { log_system_request_failed!("runtime_capabilities", "build_runtime_capabilities_failed", err); @@ -1201,7 +1201,7 @@ impl Operation for RuntimeCapabilitiesHandler { header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_system_response_emitted!("runtime_capabilities"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -1217,13 +1217,13 @@ pub(crate) fn data_usage_info_gate_actions() -> Vec { #[async_trait::async_trait] impl Operation for DataUsageInfoHandler { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { log_system_request_rejected!("query_data_usage_info", "missing_credentials"); return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, data_usage_info_gate_actions()).await?; + authorize_admin_envelope(req.envelope(), data_usage_info_gate_actions()).await?; let usecase = default_admin_usecase(); let info = usecase.execute_query_data_usage_info().await.map_err(S3Error::from)?; @@ -1237,7 +1237,7 @@ impl Operation for DataUsageInfoHandler { header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); log_system_response_emitted!("query_data_usage_info"); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -1252,7 +1252,8 @@ mod tests { build_runtime_capabilities_summary, data_usage_info_gate_actions, runtime_capabilities_gate_actions, system_admin_discovery, }; - use crate::admin::router::Operation; + use crate::admin::operation::AdminRequest; + use crate::admin::operation::Operation; use crate::admin::runtime_sources::DefaultAdminUsecase; use crate::admin::storage_api::cluster::{ CapabilityState, CapabilityStatus, MemorySamplingState, ObservabilitySnapshot, PlatformSupport, TopologyCapabilities, @@ -1264,9 +1265,9 @@ mod tests { use rustfs_concurrency::WorkloadClass; use rustfs_madmin::{InfoMessage, StorageInfo}; use rustfs_policy::policy::action::{Action, AdminAction, S3Action}; + use rustfs_s3_types::Body; use rustfs_s3_types::S3ErrorCode; use rustfs_security_governance::HttpMethod; - use s3s::{Body, S3Request}; use serde_json::json; /// Authz regression pin (rustfs/backlog#1306): datausageinfo stays an @@ -1662,17 +1663,12 @@ mod tests { #[tokio::test] async fn runtime_capabilities_authentication_failure_is_not_reported_as_unsupported() { - let request = S3Request { - input: Body::empty(), - method: Method::GET, - uri: Uri::from_static("/rustfs/admin/v4/runtime/capabilities"), - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; + let request = + crate::admin::operation::test_support::TestRequest::new(Method::GET, "/rustfs/admin/v4/runtime/capabilities") + .headers(HeaderMap::new()) + .extensions(Extensions::new()) + .body(Body::empty()) + .build(); let error = RuntimeCapabilitiesHandler {} .call(request, Params::new()) @@ -1726,26 +1722,18 @@ mod tests { Arc::new(rustfs_kms::KmsServiceManager::new()), ))); - let request = S3Request { - input: Body::empty(), - method: Method::GET, - uri: Uri::from_static("/rustfs/admin/v3/info"), - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: Some(s3s::auth::Credentials { - access_key: ROOT_ACCESS_KEY.to_string(), - secret_key: s3s::auth::SecretKey::from(ROOT_SECRET_KEY.to_string()), - }), - region: None, - service: None, - trailing_headers: None, - }; + let request = crate::admin::operation::test_support::TestRequest::new(Method::GET, "/rustfs/admin/v3/info") + .headers(HeaderMap::new()) + .extensions(Extensions::new()) + .body(Body::empty()) + .credentials(ROOT_ACCESS_KEY, ROOT_SECRET_KEY, None) + .build(); - let (status, body) = super::ServerInfoHandler {} + let (status, _headers, body) = super::ServerInfoHandler {} .call(request, Params::new()) .await .expect("root admin credentials must be served server info") - .output; + .into_parts(); assert_eq!(status, hyper::StatusCode::OK); let bytes = body.collect().await.expect("server info body should read").to_bytes(); @@ -2018,18 +2006,12 @@ mod tests { ); } - fn credential_less_request(method: Method, uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn credential_less_request(method: Method, uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(Extensions::new()) + .body(Body::empty()) + .build() } async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str) { @@ -2081,7 +2063,7 @@ mod tests { } /// Pins the gate wiring: each system handler authorizes through exactly one - /// `authorize_admin_request` call carrying the same action vector it used + /// `authorize_admin_envelope` call carrying the same action vector it used /// before the deduplication. The two gate-action helpers must be passed /// through by name so the vectors pinned by the tests above keep governing /// the live gate (rustfs/backlog#1829). @@ -2115,7 +2097,7 @@ mod tests { ] { let block = source_block(production, &format!("impl Operation for {handler}")); assert_eq!( - block.matches("authorize_admin_request(").count(), + block.matches("authorize_admin_envelope(").count(), 1, "{handler} must use exactly one shared gate" ); @@ -2128,7 +2110,7 @@ mod tests { assert!(block.contains(token), "{handler} must authorize with {token}"); } assert!( - !block.contains("let cred = authorize_admin_request("), + !block.contains("let cred = authorize_admin_envelope("), "{handler} does not consume the authenticated credentials" ); } diff --git a/rustfs/src/admin/handlers/tier.rs b/rustfs/src/admin/handlers/tier.rs index dd289c1e3..73cf5dbf8 100644 --- a/rustfs/src/admin/handlers/tier.rs +++ b/rustfs/src/admin/handlers/tier.rs @@ -13,7 +13,8 @@ // limitations under the License. #![allow(unused_variables, unused_mut, unused_must_use)] -use crate::admin::runtime_sources::object_store_from_extensions; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; +use crate::admin::runtime_sources::object_store_from_envelope; use crate::admin::storage_api::runtime_sources::TierConfigMgr; use crate::admin::storage_api::tier::{ AdminError, ClusterTierDailyStats, DailyAllTierStats, ECStore, ERR_TIER_ALREADY_EXISTS, ERR_TIER_BACKEND_IN_USE, @@ -24,11 +25,12 @@ use crate::admin::storage_api::tier::{ use crate::{ admin::runtime_sources::{current_daily_tier_stats, current_notification_system, current_tier_config_handle}, admin::{ - auth::authorize_admin_request, - router::{AdminOperation, Operation, S3Router}, + auth::authorize_admin_envelope, + router::{AdminOperation, S3Router}, }, server::ADMIN_PREFIX, }; +use http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use http::{HeaderMap, StatusCode, Uri}; use hyper::Method; use matchit::Params; @@ -36,11 +38,7 @@ use percent_encoding::percent_decode_str; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; use rustfs_data_usage::TierStats; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; -use s3s::{ - Body, S3Request, S3Response, - header::{CONTENT_LENGTH, CONTENT_TYPE}, -}; +use rustfs_s3_types::{Body, S3Error, S3ErrorCode, S3Result, s3_error}; use serde_urlencoded::from_bytes; use std::collections::{BTreeSet, HashMap, HashSet}; use std::sync::Arc; @@ -308,9 +306,9 @@ pub fn register_tier_route(r: &mut S3Router) -> std::io::Result< #[async_trait::async_trait] impl Operation for AddTier { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddTierQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -319,13 +317,13 @@ impl Operation for AddTier { } }; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; - let mut input = req.input; + let mut input = req.take_body(); let body = match input.store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE).await { Ok(b) => b, Err(e) => { @@ -374,7 +372,7 @@ impl Operation for AddTier { s3_error!(InvalidRequest, "invalid force flag") })?; } - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object store is not initialized")); }; @@ -435,16 +433,16 @@ impl Operation for AddTier { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } pub struct EditTier {} #[async_trait::async_trait] impl Operation for EditTier { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddTierQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -453,13 +451,13 @@ impl Operation for EditTier { } }; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; - let mut input = req.input; + let mut input = req.take_body(); let body = match input.store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE).await { Ok(b) => b, Err(e) => { @@ -491,7 +489,7 @@ impl Operation for EditTier { "admin tier state" ); - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object store is not initialized")); }; @@ -527,7 +525,7 @@ impl Operation for EditTier { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } @@ -539,9 +537,9 @@ pub struct BucketQuery { pub struct ListTiers {} #[async_trait::async_trait] impl Operation for ListTiers { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: BucketQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get query failed"))?; input @@ -550,11 +548,11 @@ impl Operation for ListTiers { } }; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ListTierAction)]).await?; let tier_config_mgr_handle = current_tier_config_handle(); let tier_config_mgr = tier_config_mgr_handle.read().await; @@ -566,16 +564,16 @@ impl Operation for ListTiers { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } pub struct RemoveTier {} #[async_trait::async_trait] impl Operation for RemoveTier { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddTierQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -584,11 +582,11 @@ impl Operation for RemoveTier { } }; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; let mut force: bool = false; let force_str = query.force.clone().unwrap_or_default(); @@ -609,7 +607,7 @@ impl Operation for RemoveTier { let tier_name = params.get("tiername").map(|s| s.to_string()).unwrap_or_default(); - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object store is not initialized")); }; @@ -652,21 +650,21 @@ impl Operation for RemoveTier { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } pub struct VerifyTier {} #[async_trait::async_trait] impl Operation for VerifyTier { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ListTierAction)]).await?; - let tier = resolve_tier_name(&req.uri, ¶ms)?; + let tier = resolve_tier_name(req.uri(), ¶ms)?; let tier_config_mgr_handle = current_tier_config_handle(); TierConfigMgr::verify_without_manager_lock(&tier_config_mgr_handle, &tier) .await @@ -675,7 +673,7 @@ impl Operation for VerifyTier { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } @@ -783,15 +781,15 @@ struct TierStatsBodyV2 { pub struct GetTierInfo {} #[async_trait::async_trait] impl Operation for GetTierInfo { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ListTierAction)]).await?; let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: TierStatsQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -809,7 +807,7 @@ impl Operation for GetTierInfo { let data = if tier_stats_wants_legacy_format(&query)? { serde_json::to_vec(&filter_tier_stats(current_daily_tier_stats(), tier_name)) } else { - let store = object_store_from_extensions(&req.extensions); + let store = object_store_from_envelope(req.envelope()); serde_json::to_vec(&tier_stats_body(store, tier_name).await) } .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal tier err {e}")))?; @@ -817,7 +815,7 @@ impl Operation for GetTierInfo { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -1064,14 +1062,14 @@ fn parse_clear_tier_query(uri: &Uri) -> S3Result { pub struct ClearTier {} #[async_trait::async_trait] impl Operation for ClearTier { - async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { - let query = parse_clear_tier_query(&req.uri)?; + async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result { + let query = parse_clear_tier_query(req.uri())?; - if req.credentials.is_none() { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::SetTierAction)]).await?; let mut force: bool = false; let force_str = query.force; @@ -1098,7 +1096,7 @@ impl Operation for ClearTier { return Err(s3_error!(InvalidRequest, "invalid clear-tier confirmation token")); }; - let Some(store) = object_store_from_extensions(&req.extensions) else { + let Some(store) = object_store_from_envelope(req.envelope()) else { return Err(s3_error!(InternalError, "object store is not initialized")); }; @@ -1149,7 +1147,7 @@ impl Operation for ClearTier { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } @@ -1736,18 +1734,12 @@ mod tests { stats } - fn credential_less_request(method: Method, uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: http::Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn credential_less_request(method: Method, uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(http::Extensions::new()) + .body(Body::empty()) + .build() } async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str, message: &str) { @@ -1787,7 +1779,7 @@ mod tests { fn assert_shared_gate_wiring(block: &str, item: &str, actions: &[&str], binds_credentials: bool) { assert_eq!( - block.matches("authorize_admin_request(").count(), + block.matches("authorize_admin_envelope(").count(), 1, "{item} must use exactly one shared gate" ); @@ -1800,7 +1792,7 @@ mod tests { assert!(block.contains(&format!("AdminAction::{action}")), "{item} must authorize with {action}"); } assert_eq!( - block.contains("let cred = authorize_admin_request("), + block.contains("let cred = authorize_admin_envelope("), binds_credentials, "{item} credential binding must match its payload-processing contract" ); diff --git a/rustfs/src/admin/handlers/user.rs b/rustfs/src/admin/handlers/user.rs index e900045db..caeb6bbb0 100644 --- a/rustfs/src/admin/handlers/user.rs +++ b/rustfs/src/admin/handlers/user.rs @@ -14,17 +14,18 @@ use super::iam_error::iam_error_to_s3_error; use super::{account_info, group, service_account, user_iam, user_lifecycle, user_policy_binding}; +use crate::admin::operation::{AdminRequest, AdminResponse, Operation}; use crate::{ admin::runtime_sources::current_action_credentials, admin::{ - auth::{authorize_admin_request, validate_admin_request}, + auth::{authorize_admin_envelope, validate_admin_request}, handlers::site_replication::site_replication_iam_change_hook, - router::{AdminOperation, Operation, S3Router}, + router::{AdminOperation, S3Router}, utils::{encode_compatible_admin_payload, has_space_be, read_compatible_admin_body}, }, auth::{check_key_valid, constant_time_eq, get_session_token}, - server::RemoteAddr, }; +use http::header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE}; use http::{HeaderMap, StatusCode}; use matchit::Params; use rustfs_config::{MAX_ADMIN_REQUEST_BODY_SIZE, MAX_IAM_IMPORT_EXPANDED_SIZE, MAX_IAM_IMPORT_SIZE}; @@ -39,12 +40,8 @@ use rustfs_madmin::{ user::{ImportIAMResult, SRSessionPolicy, SRSvcAccCreate}, }; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; +use rustfs_s3_types::{Body, S3Error, S3ErrorCode, S3Result, s3_error}; use rustfs_utils::path::path_join_buf; -use s3s::{ - Body, S3Request, S3Response, - header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE}, -}; use serde::Deserialize; use serde_urlencoded::from_bytes; use std::io::{Read as _, Write}; @@ -214,9 +211,9 @@ fn map_add_user_create_error(err: rustfs_iam::error::Error) -> S3Error { #[async_trait::async_trait] impl Operation for AddUser { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddUserQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -225,12 +222,12 @@ impl Operation for AddUser { } }; - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; let ak = query.access_key.as_deref().unwrap_or_default(); @@ -238,7 +235,8 @@ impl Operation for AddUser { return Err(s3_error!(InvalidArgument, "access key is empty")); } - let body = read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), &cred.secret_key).await?; + let body = + read_compatible_admin_body(req.take_body(), MAX_ADMIN_REQUEST_BODY_SIZE, req.uri().path(), &cred.secret_key).await?; // let body_bytes = decrypt_data(input_cred.secret_key.expose().as_bytes(), &body) // .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, format!("decrypt_data err {}", e)))?; @@ -301,12 +299,12 @@ impl Operation for AddUser { // For eligible self operations, only explicit Deny should block the request. validate_admin_request( - &req.headers, + req.headers(), &cred, owner, check_deny_only, vec![Action::AdminAction(AdminAction::CreateUserAdminAction)], - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), + req.remote_addr(), ) .await?; @@ -341,16 +339,16 @@ impl Operation for AddUser { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } pub struct SetUserStatus {} #[async_trait::async_trait] impl Operation for SetUserStatus { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddUserQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -365,15 +363,15 @@ impl Operation for SetUserStatus { return Err(s3_error!(InvalidArgument, "access key is empty")); } - let Some(input_cred) = req.credentials.as_ref() else { + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; - if constant_time_eq(&input_cred.access_key, ak) { + if constant_time_eq(input_cred.access_key(), ak) { return Err(s3_error!(InvalidArgument, "cannot change the status of the current user")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::EnableUserAdminAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::EnableUserAdminAction)]).await?; let status = AccountStatus::try_from(query.status.as_deref().unwrap_or_default()) .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, e))?; @@ -420,7 +418,7 @@ impl Operation for SetUserStatus { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } @@ -432,15 +430,15 @@ pub struct BucketQuery { pub struct ListUsers {} #[async_trait::async_trait] impl Operation for ListUsers { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - let cred = authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ListUsersAdminAction)]).await?; + let cred = authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ListUsersAdminAction)]).await?; let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: BucketQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -469,27 +467,28 @@ impl Operation for ListUsers { let data = serde_json::to_vec(&users) .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to serialize response: {e}")))?; - let (data, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, data)?; + let (data, content_type) = encode_compatible_admin_payload(req.uri().path(), &cred.secret_key, data)?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } pub struct RemoveUser {} #[async_trait::async_trait] impl Operation for RemoveUser { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "authentication required")); } - let cred = authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::DeleteUserAdminAction)]).await?; + let actions = vec![Action::AdminAction(AdminAction::DeleteUserAdminAction)]; + let cred = authorize_admin_envelope(req.envelope(), actions).await?; let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddUserQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -557,16 +556,16 @@ impl Operation for RemoveUser { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::empty(), header)) } } pub struct GetUserInfo {} #[async_trait::async_trait] impl Operation for GetUserInfo { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { let query = { - if let Some(query) = req.uri.query() { + if let Some(query) = req.uri().query() { let input: AddUserQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?; input @@ -585,12 +584,12 @@ impl Operation for GetUserInfo { return Err(s3_error!(InternalError, "iam is not initialized")); }; - let Some(input_cred) = req.credentials else { + let Some(input_cred) = req.credentials() else { return Err(s3_error!(InvalidRequest, "authentication required")); }; let (cred, owner) = - check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; let check_deny_only = should_check_deny_only(ak, &cred); @@ -614,12 +613,12 @@ impl Operation for GetUserInfo { // For eligible self operations, only explicit Deny should block the request. validate_admin_request( - &req.headers, + req.headers(), &cred, owner, check_deny_only, vec![Action::AdminAction(AdminAction::GetUserAdminAction)], - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), + req.remote_addr(), ) .await?; @@ -631,7 +630,7 @@ impl Operation for GetUserInfo { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(data), header)) } } @@ -659,12 +658,12 @@ const IAM_EXPORT_FILES: &[&str] = &[ pub struct ExportIam {} #[async_trait::async_trait] impl Operation for ExportIam { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - if req.credentials.is_none() { + async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + if req.credentials().is_none() { return Err(s3_error!(InvalidRequest, "get cred failed")); } - authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ExportIAMAction)]).await?; + authorize_admin_envelope(req.envelope(), vec![Action::AdminAction(AdminAction::ExportIAMAction)]).await?; let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else { return Err(s3_error!(InvalidRequest, "iam not init")); @@ -858,7 +857,7 @@ impl Operation for ExportIam { "attachment; filename=iam-assets.zip".parse().expect("valid header value"), ); header.insert(CONTENT_LENGTH, zip_bytes.get_ref().len().to_string().parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(zip_bytes.into_inner())), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(zip_bytes.into_inner()), header)) } } @@ -890,25 +889,25 @@ fn read_import_member(file: &mut impl std::io::Read, budget: &mut u64) -> S3Resu pub struct ImportIam {} #[async_trait::async_trait] impl Operation for ImportIam { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - let Some(input_cred) = req.credentials else { + async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result { + let Some(input_cred) = req.credentials() 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?; + check_key_valid(get_session_token(req.uri(), req.headers()).unwrap_or_default(), input_cred.access_key()).await?; validate_admin_request( - &req.headers, + req.headers(), &cred, owner, false, vec![Action::AdminAction(AdminAction::ImportIAMAction)], - req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), + req.remote_addr(), ) .await?; - let mut input = req.input; + let mut input = req.take_body(); let body = match input.store_all_limited(MAX_IAM_IMPORT_SIZE).await { Ok(b) => b, Err(e) => { @@ -1341,7 +1340,7 @@ impl Operation for ImportIam { let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value")); - Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header)) + Ok(AdminResponse::with_headers(StatusCode::OK, Body::from(body), header)) } } @@ -1355,27 +1354,22 @@ mod tests { map_add_user_create_error, read_import_member, should_check_deny_only, should_reject_group_import_name, should_restore_group_as_disabled, }; + use crate::admin::operation::AdminRequest; use http::{Extensions, Method, Uri}; use rustfs_credentials::{Credentials, IAM_POLICY_CLAIM_NAME_SA}; use rustfs_iam::error::Error as IamError; use rustfs_madmin::user::SRSvcAccCreate; + use rustfs_s3_types::Body; use rustfs_s3_types::S3ErrorCode; - use s3s::{Body, S3Request}; use serde_json::Value; use std::collections::HashMap; - fn credential_less_request(method: Method, uri: &'static str) -> S3Request { - S3Request { - input: Body::empty(), - method, - uri: Uri::from_static(uri), - headers: HeaderMap::new(), - extensions: Extensions::new(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - } + fn credential_less_request(method: Method, uri: &'static str) -> AdminRequest { + crate::admin::operation::test_support::TestRequest::new(method, Uri::from_static(uri)) + .headers(HeaderMap::new()) + .extensions(Extensions::new()) + .body(Body::empty()) + .build() } async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str, message: &str) { @@ -1402,7 +1396,7 @@ mod tests { fn assert_shared_gate_wiring(block: &str, item: &str, actions: &[&str], binds_credentials: bool) { assert_eq!( - block.matches("authorize_admin_request(").count(), + block.matches("authorize_admin_envelope(").count(), 1, "{item} must use exactly one shared gate" ); @@ -1415,7 +1409,7 @@ mod tests { assert!(block.contains(&format!("AdminAction::{action}")), "{item} must authorize with {action}"); } assert_eq!( - block.contains("let cred = authorize_admin_request("), + block.contains("let cred = authorize_admin_envelope("), binds_credentials, "{item} credential binding must match its payload-processing contract" ); @@ -1959,7 +1953,7 @@ mod tests { "{handler} must keep the context that rules out the shared gate ({marker})" ); assert!( - !block.contains("authorize_admin_request("), + !block.contains("authorize_admin_envelope("), "{handler} must not be routed through the shared gate" ); assert!( diff --git a/rustfs/src/admin/runtime_sources.rs b/rustfs/src/admin/runtime_sources.rs index 86f3f0ee0..5776c6d20 100644 --- a/rustfs/src/admin/runtime_sources.rs +++ b/rustfs/src/admin/runtime_sources.rs @@ -66,14 +66,6 @@ pub(crate) fn current_object_data_cache() -> Option> current_object_data_cache_handle_for_context(context.as_deref()) } -/// Resolve the object store for an admin request through the server's context -/// slot injected at router dispatch (backlog#1052 S2). Falls back to the -/// ambient process context when no slot was injected (direct handler tests, -/// paths outside the router) — the single-instance legacy default. -pub(crate) fn object_store_from_req(req: &s3s::S3Request) -> Option> { - object_store_from_extensions(&req.extensions) -} - pub(crate) fn app_context_from_req(req: &s3s::S3Request) -> Option> { app_context_from_extensions(&req.extensions) } @@ -85,8 +77,10 @@ pub(crate) fn app_context_from_envelope(envelope: &RequestEnvelope) -> Option Option> { match envelope.server_context() { Some(slot) => slot.installed_object_store(), @@ -107,15 +101,6 @@ pub(crate) fn app_context_from_extensions(extensions: &http::Extensions) -> Opti } } -/// Field-borrow form of [`object_store_from_req`] for handlers that have -/// already moved other request fields (body, credentials) out of the request. -pub(crate) fn object_store_from_extensions(extensions: &http::Extensions) -> Option> { - match extensions.get::>() { - Some(slot) => slot.installed_object_store(), - None => current_object_store_handle(), - } -} - pub(crate) fn current_notification_system() -> Option> { let context = current_app_context(); current_notification_system_for_context(context.as_deref()) @@ -175,8 +160,7 @@ pub(crate) fn publish_storage_class_config(config: StorageClassConfig) { mod tests { use super::{ AppContext, IamInterface, KmsInterface, ServerContextSlot, app_context_from_envelope, app_context_from_extensions, - app_context_from_req, current_app_context, object_store_from_envelope, object_store_from_extensions, - publish_test_app_context, + app_context_from_req, current_app_context, object_store_from_envelope, publish_test_app_context, }; use crate::admin::router::{Operation, S3Router}; use crate::app::RequestEnvelope; @@ -256,7 +240,7 @@ mod tests { extensions.insert(slot); assert!(app_context_from_extensions(&extensions).is_none()); - assert!(object_store_from_extensions(&extensions).is_none()); + assert!(object_store_from_envelope(&envelope(extensions.clone())).is_none()); assert!(app_context_from_req(&request(extensions)).is_none()); assert!(Arc::ptr_eq( ¤t_app_context().expect("ambient context must remain available"), @@ -287,7 +271,7 @@ mod tests { let resolved = app_context_from_extensions(&extensions).expect("legacy request must use ambient context"); assert!(Arc::ptr_eq(&resolved, &ambient)); - assert!(object_store_from_extensions(&extensions).is_some()); + assert!(object_store_from_envelope(&envelope(extensions)).is_some()); } #[tokio::test] diff --git a/rustfs/src/admin/storage_api.rs b/rustfs/src/admin/storage_api.rs index c712f9f55..5ebe6b415 100644 --- a/rustfs/src/admin/storage_api.rs +++ b/rustfs/src/admin/storage_api.rs @@ -1070,7 +1070,7 @@ pub(crate) mod s3 { pub(crate) use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result}; #[allow(unused_imports)] pub(crate) use s3s::auth; - pub(crate) use s3s::{Body, S3Request, S3Response, header}; + pub(crate) use s3s::header; /// Build an `S3Error` without reaching for the `s3s` error macro. /// diff --git a/rustfs/src/admin/utils.rs b/rustfs/src/admin/utils.rs index b9f95bd74..c4578113c 100644 --- a/rustfs/src/admin/utils.rs +++ b/rustfs/src/admin/utils.rs @@ -14,11 +14,10 @@ use crate::admin::operation::AdminResponse; use crate::server::{MINIO_ADMIN_PREFIX, has_path_prefix}; +use http::header::CONTENT_TYPE; use http::{HeaderMap, HeaderValue, StatusCode, Uri}; use rustfs_crypto::{decrypt_data, decrypt_stream_io, encrypt_stream_io}; use rustfs_s3_types::{S3Error, S3ErrorCode, S3Result, s3_error}; -use s3s::header::CONTENT_TYPE; -use s3s::{Body, S3Response}; use serde::Serialize; use std::collections::HashMap; @@ -79,16 +78,6 @@ pub(crate) fn encode_compatible_admin_payload(path: &str, secret_key: &str, data /// the body. Serialization failure is reported as `InternalError`; the /// response structs the admin handlers pass here are plain owned data, so that /// arm is unreachable in practice. -pub(crate) fn json_response(status: StatusCode, value: &T) -> S3Result> { - let data = serde_json::to_vec(value) - .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to serialize response: {e}")))?; - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - Ok(S3Response::with_headers((status, Body::from(data)), headers)) -} - -/// Owned-boundary form of [`json_response`]: the same single `Content-Type` -/// header and the same serialized bytes, as an [`AdminResponse`]. pub(crate) fn owned_json_response(status: StatusCode, value: &T) -> S3Result { let data = serde_json::to_vec(value) .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to serialize response: {e}")))?; @@ -127,7 +116,6 @@ pub(crate) fn extract_query_params(uri: &Uri) -> HashMap { mod tests { use super::*; use rustfs_crypto::encrypt_data; - use s3s::Body; #[test] fn has_space_be_detects_any_whitespace() { @@ -177,30 +165,13 @@ mod tests { let payload = b"{\"ok\":true}".to_vec(); let encrypted = encrypt_data(b"secret", &payload).expect("encrypt payload"); - let decoded = read_compatible_admin_body(Body::from(encrypted), 1024, "/minio/admin/v3/list-users", "secret") + let decoded = read_compatible_admin_body(s3s::Body::from(encrypted), 1024, "/minio/admin/v3/list-users", "secret") .await .expect("decode payload"); assert_eq!(decoded, payload); } - #[tokio::test] - async fn owned_json_response_matches_the_legacy_shape() { - let value = serde_json::json!({"ok": true, "count": 3}); - let mut legacy = json_response(StatusCode::CREATED, &value).expect("legacy serializes"); - let (status, headers, mut body) = owned_json_response(StatusCode::CREATED, &value) - .expect("owned serializes") - .into_parts(); - - assert_eq!(status, legacy.output.0); - assert_eq!(headers, legacy.headers); - assert_eq!(headers.len(), 1); - assert_eq!(headers.get(CONTENT_TYPE).and_then(|v| v.to_str().ok()), Some("application/json")); - let legacy_bytes = legacy.output.1.store_all_limited(1024).await.expect("legacy body reads"); - let owned_bytes = body.store_all_limited(1024).await.expect("owned body reads"); - assert_eq!(owned_bytes, legacy_bytes); - } - #[tokio::test] async fn owned_empty_response_carries_only_the_status() { let (status, headers, mut body) = owned_empty_response(StatusCode::NO_CONTENT).into_parts(); @@ -220,7 +191,7 @@ mod tests { assert_eq!(err.code(), &S3ErrorCode::InvalidRequest); assert_eq!(err.message(), Some("failed to read request body: length limit exceeded")); - let legacy_body = Body::http_body(http_body_util::Full::new(bytes::Bytes::from_static(b"nine bytes!"))); + let legacy_body = s3s::Body::http_body(http_body_util::Full::new(bytes::Bytes::from_static(b"nine bytes!"))); let legacy_err = read_compatible_admin_body(legacy_body, 8, "/rustfs/admin/v3/list-users", "secret") .await .expect_err("legacy streaming bodies retain the same limit error"); @@ -243,7 +214,7 @@ mod tests { assert_eq!(stream_err.message(), Some("failed to read request body: transport failed")); } - async fn body_bytes(mut body: Body) -> Vec { + async fn body_bytes(mut body: rustfs_s3_types::Body) -> Vec { body.store_all_limited(64 * 1024).await.expect("body should read").to_vec() } @@ -251,35 +222,33 @@ mod tests { /// the requested status, `application/json`, and the serialized bytes /// verbatim — nothing else. #[tokio::test] - async fn json_response_carries_status_content_type_and_serialized_body() { + async fn owned_json_response_carries_status_content_type_and_serialized_body() { #[derive(Serialize)] struct Payload { success: bool, message: &'static str, } - let response = json_response( + let (status, headers, body) = owned_json_response( StatusCode::ACCEPTED, &Payload { success: true, message: "queued", }, ) - .expect("payload should serialize"); + .expect("payload should serialize") + .into_parts(); - assert_eq!(response.output.0, StatusCode::ACCEPTED); - assert_eq!( - response.headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()), - Some("application/json") - ); - assert_eq!(response.headers.len(), 1); - assert_eq!(body_bytes(response.output.1).await, br#"{"success":true,"message":"queued"}"#.to_vec()); + assert_eq!(status, StatusCode::ACCEPTED); + assert_eq!(headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()), Some("application/json")); + assert_eq!(headers.len(), 1); + assert_eq!(body_bytes(body).await, br#"{"success":true,"message":"queued"}"#.to_vec()); } /// A serialization failure must surface as `InternalError` rather than a /// panic or a half-written body. #[test] - fn json_response_reports_serialization_failure_as_internal_error() { + fn owned_json_response_reports_serialization_failure_as_internal_error() { struct Unserializable; impl Serialize for Unserializable { @@ -288,7 +257,7 @@ mod tests { } } - let err = json_response(StatusCode::OK, &Unserializable).expect_err("serialization must fail"); + let err = owned_json_response(StatusCode::OK, &Unserializable).expect_err("serialization must fail"); assert_eq!(err.code(), &S3ErrorCode::InternalError); assert!(err.message().unwrap_or_default().contains("failed to serialize response")); }