mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-27 14:27:10 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Body>, actions: Vec<Action>) -> S3Result<Credentials> {
|
||||
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::<Option<RemoteAddr>>().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<Action>) -> S3Result<Credentials> {
|
||||
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(
|
||||
|
||||
@@ -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<AdminOperation>) -> std::io::
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for ExportBucketMetadata {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<u8>) -> S3Request<Body> {
|
||||
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<u8>) -> 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<u8>) {
|
||||
@@ -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<Body> {
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -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<AdminOperation>) ->
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RecoverOrphanedBucketHandler {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::RecoverOrphanedBucketAction)]).await?;
|
||||
async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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(),
|
||||
|
||||
@@ -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<Body>) -> 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<S3Response<(StatusCode, Body)>> {
|
||||
fn build_json_response(status: StatusCode, body: &impl Serialize, request_id: Option<&HeaderValue>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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
|
||||
|
||||
@@ -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<s3s::Body>,
|
||||
params: Params<'_, '_>,
|
||||
) -> S3Result<s3s::S3Response<(StatusCode, s3s::Body)>> {
|
||||
impl Operation for ImportIamV2 {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
// 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<rustfs_config::server_config::Config
|
||||
/// provides, exactly as it is for other externally-provisioned identities.
|
||||
pub struct AddServiceAccountLdap {}
|
||||
|
||||
// Stays on the legacy trait until `service_account::AddServiceAccount`, which
|
||||
// it delegates to, serves the owned request.
|
||||
#[async_trait::async_trait]
|
||||
impl LegacyOperation for AddServiceAccountLdap {
|
||||
async fn call(
|
||||
&self,
|
||||
req: s3s::S3Request<s3s::Body>,
|
||||
params: Params<'_, '_>,
|
||||
) -> S3Result<s3s::S3Response<(StatusCode, s3s::Body)>> {
|
||||
impl Operation for AddServiceAccountLdap {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
AddServiceAccount {}.call(req, params).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<Option<RemoteAddr>>()
|
||||
.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!(
|
||||
|
||||
@@ -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<S3Response<(StatusCode, Body)>> {
|
||||
fn failure_response(failure: Failure) -> S3Result<AdminResponse> {
|
||||
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<S3Response<(StatusCode, Body)>
|
||||
/// 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<Body>,
|
||||
mut req: AdminRequest,
|
||||
actions: Vec<Action>,
|
||||
operation: KmsAdminOperation,
|
||||
) -> S3Result<(KmsAdminAudit, Vec<u8>)> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(cred) = req.credentials else {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(cred) = req.credentials else {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(cred) = req.credentials else {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let context = app_context_from_req(&req)
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(cred) = req.credentials else {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(cred) = req.credentials else {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T: serde::Serialize>(
|
||||
path: &str,
|
||||
secret_key: &str,
|
||||
status: http::StatusCode,
|
||||
payload: &T,
|
||||
) -> rustfs_s3_types::S3Result<s3s::S3Response<(http::StatusCode, s3s::Body)>> {
|
||||
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<T: serde::Serialize>(
|
||||
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]
|
||||
|
||||
@@ -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<ModuleSwitchSnapshot> for ModuleSwitchesResponse {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_response<T: Serialize>(
|
||||
status: StatusCode,
|
||||
body: &T,
|
||||
request_id: Option<&http::HeaderValue>,
|
||||
) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
fn build_response<T: Serialize>(status: StatusCode, body: &T, request_id: Option<&http::HeaderValue>) -> S3Result<AdminResponse> {
|
||||
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<Body>, 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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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
|
||||
|
||||
@@ -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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, _req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, provider_id: &str) -> S3Result<String> {
|
||||
fn derive_callback_uri(req: &AdminRequest, provider_id: &str) -> S3Result<String> {
|
||||
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<Body>,
|
||||
req: &AdminRequest,
|
||||
provider_id: &str,
|
||||
policy: Option<&rustfs_iam::federation::FederatedRedirectPolicy>,
|
||||
) -> S3Result<String> {
|
||||
@@ -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<Body>,
|
||||
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<Body>) -> S3Result<String> {
|
||||
fn build_console_login_redirect(req: &AdminRequest) -> S3Result<String> {
|
||||
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<Body>) -> S3Result<String> {
|
||||
Ok(format!("{scheme}://{host}{login_path}"))
|
||||
}
|
||||
|
||||
fn redirect_response(location: &str) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let mut resp = S3Response::new((StatusCode::FOUND, Body::empty()));
|
||||
resp.headers.insert(
|
||||
fn redirect_response(location: &str) -> S3Result<AdminResponse> {
|
||||
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<Body>, 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<T: DeserializeOwned>(req: &mut S3Request<Body>) -> S3Result<T> {
|
||||
async fn parse_json_body<T: DeserializeOwned>(req: &mut AdminRequest) -> S3Result<T> {
|
||||
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<T: DeserializeOwned>(req: &mut S3Request<Body>) -> S3Re
|
||||
serde_json::from_slice(&body).map_err(|e| s3_error!(InvalidRequest, "invalid JSON: {}", e))
|
||||
}
|
||||
|
||||
fn extract_request_scheme(req: &S3Request<Body>) -> S3Result<String> {
|
||||
fn extract_request_scheme(req: &AdminRequest) -> S3Result<String> {
|
||||
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<Body>) -> S3Result<String> {
|
||||
Ok(raw_scheme)
|
||||
}
|
||||
|
||||
fn extract_request_host(req: &S3Request<Body>) -> S3Result<String> {
|
||||
fn extract_request_host(req: &AdminRequest) -> S3Result<String> {
|
||||
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<Body> {
|
||||
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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Body>) -> Option<String> {
|
||||
req.extensions
|
||||
.get::<Option<RemoteAddr>>()
|
||||
.and_then(|opt| opt.map(|addr| addr.0.to_string()))
|
||||
fn admin_remote_addr(req: &AdminRequest) -> Option<String> {
|
||||
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 <endpoint>/<admin-API>/pools/list
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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 <endpoint>/<admin-API>/pools/status?pool=http://server{1...4}/disk{1...4}
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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 <endpoint>/<admin-API>/decommission/status[?pool=http://server{1...4}/disk{1...4}]
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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 <endpoint>/<admin-API>/pools/decommission?pool=http://server{1...4}/disk{1...4}
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string();
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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 <endpoint>/<admin-API>/pools/cancel?pool=http://server{1...4}/disk{1...4}
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string();
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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 <endpoint>/<admin-API>/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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string();
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Body>) -> 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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
authorize_profile_request(&req).await?;
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
authorize_profile_request(&req).await?;
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body> {
|
||||
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]
|
||||
|
||||
@@ -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<AdminOperation>) -> std::io::Result
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for SetBucketQuotaHandler {
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Result<Bytes, StdError>>,
|
||||
}
|
||||
|
||||
impl Stream for MetricsStream {
|
||||
type Item = Result<Bytes, StdError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
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<Body>) -> 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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
debug!("handle MetricsHandler, uri: {:?}, params: {:?}", req.uri, params);
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Result<Bytes, StdError>>(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<Body> {
|
||||
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]
|
||||
|
||||
@@ -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<Body>) -> Option<String> {
|
||||
req.extensions
|
||||
.get::<Option<RemoteAddr>>()
|
||||
.and_then(|opt| opt.map(|addr| addr.0.to_string()))
|
||||
fn admin_remote_addr(req: &AdminRequest) -> Option<String> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string();
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string();
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let request_id = admin_request_id(&req.headers).unwrap_or_default().to_string();
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<AdminOperation>) -> std::
|
||||
pub struct AddServiceAccount {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for AddServiceAccount {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(req_cred) = req.credentials else {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<T: IamStore>(
|
||||
pub struct UpdateServiceAccount {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for UpdateServiceAccount {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let query = { parse_list_access_keys_query(req.uri.query()) };
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(input_cred) = req.credentials else {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<const N: usize>(
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RuntimeCapabilitiesHandler {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Action> {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for DataUsageInfoHandler {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<AdminOperation>) -> std::io::Result<
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for AddTier {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<ClearTierQuery> {
|
||||
pub struct ClearTier {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for ClearTier {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let query = parse_clear_tier_query(&req.uri)?;
|
||||
async fn call(&self, req: AdminRequest, params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body> {
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
if req.credentials.is_none() {
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let Some(input_cred) = req.credentials else {
|
||||
async fn call(&self, mut req: AdminRequest, _params: Params<'_, '_>) -> S3Result<AdminResponse> {
|
||||
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::<Option<RemoteAddr>>().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<Body> {
|
||||
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!(
|
||||
|
||||
@@ -66,14 +66,6 @@ pub(crate) fn current_object_data_cache() -> Option<Arc<ObjectDataCacheAdapter>>
|
||||
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<B>(req: &s3s::S3Request<B>) -> Option<Arc<ECStore>> {
|
||||
object_store_from_extensions(&req.extensions)
|
||||
}
|
||||
|
||||
pub(crate) fn app_context_from_req<B>(req: &s3s::S3Request<B>) -> Option<Arc<AppContext>> {
|
||||
app_context_from_extensions(&req.extensions)
|
||||
}
|
||||
@@ -85,8 +77,10 @@ pub(crate) fn app_context_from_envelope(envelope: &RequestEnvelope) -> Option<Ar
|
||||
}
|
||||
}
|
||||
|
||||
/// Envelope form of [`object_store_from_req`]: the injected server slot wins,
|
||||
/// the ambient process store is the single-instance fallback.
|
||||
/// 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_envelope(envelope: &RequestEnvelope) -> Option<Arc<ECStore>> {
|
||||
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<Arc<ECStore>> {
|
||||
match extensions.get::<Arc<ServerContextSlot>>() {
|
||||
Some(slot) => slot.installed_object_store(),
|
||||
None => current_object_store_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_notification_system() -> Option<Arc<NotificationSys>> {
|
||||
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]
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
+14
-45
@@ -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<T: Serialize>(status: StatusCode, value: &T) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
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<T: Serialize>(status: StatusCode, value: &T) -> S3Result<AdminResponse> {
|
||||
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<String, String> {
|
||||
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<u8> {
|
||||
async fn body_bytes(mut body: rustfs_s3_types::Body) -> Vec<u8> {
|
||||
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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user