mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-29 23:37:04 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e6ef3ca07 | |||
| 6c442ea5c5 |
@@ -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=65
|
||||
files=60
|
||||
s3_error_lines=289
|
||||
ecstore_files=0
|
||||
|
||||
@@ -2293,33 +2293,21 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nested_heal_routes_still_require_authentication() {
|
||||
use s3s::route::S3Route;
|
||||
|
||||
let mut router = super::S3Router::new(false);
|
||||
super::register_heal_route(&mut router).expect("register production Heal routes");
|
||||
for prefix in ["/rustfs/admin", "/minio/admin"] {
|
||||
for object in ["dir/object.bin", "dir%2Fobject.bin", "literal%252Fslash"] {
|
||||
let mut req = s3s::S3Request {
|
||||
input: s3s::Body::empty(),
|
||||
method: http::Method::POST,
|
||||
uri: format!("{prefix}/v3/heal/test-bucket/{object}").parse().expect("Heal URI"),
|
||||
headers: http::HeaderMap::new(),
|
||||
extensions: http::Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
let uri: http::Uri = format!("{prefix}/v3/heal/test-bucket/{object}").parse().expect("Heal URI");
|
||||
let err = router
|
||||
.check_access(&mut req)
|
||||
.check_unsigned_access(http::Method::POST, uri.clone())
|
||||
.await
|
||||
.expect_err("router must require a signature");
|
||||
assert_eq!(err.code(), &S3ErrorCode::AccessDenied.into());
|
||||
assert_eq!(err.code(), &S3ErrorCode::AccessDenied);
|
||||
let err = router
|
||||
.call(req)
|
||||
.call_unsigned(http::Method::POST, uri)
|
||||
.await
|
||||
.expect_err("handler must independently require authentication");
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest.into());
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
|
||||
assert!(err.to_string().contains("authentication required"));
|
||||
}
|
||||
}
|
||||
@@ -2327,34 +2315,22 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn replacement_recovery_diagnostics_require_authentication() {
|
||||
use s3s::route::S3Route;
|
||||
|
||||
let mut router = super::S3Router::new(false);
|
||||
super::register_heal_route(&mut router).expect("register production Heal routes");
|
||||
for prefix in ["/rustfs/admin", "/minio/admin"] {
|
||||
let path = format!("{prefix}/v4/heal/replacement-recovery");
|
||||
assert!(router.contains_compatible_route(http::Method::GET, &path));
|
||||
let mut req = s3s::S3Request {
|
||||
input: s3s::Body::empty(),
|
||||
method: http::Method::GET,
|
||||
uri: path.parse().expect("replacement diagnostics URI"),
|
||||
headers: http::HeaderMap::new(),
|
||||
extensions: http::Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
let uri: http::Uri = path.parse().expect("replacement diagnostics URI");
|
||||
let err = router
|
||||
.check_access(&mut req)
|
||||
.check_unsigned_access(http::Method::GET, uri.clone())
|
||||
.await
|
||||
.expect_err("diagnostics router must require a signature");
|
||||
assert_eq!(err.code(), &S3ErrorCode::AccessDenied.into());
|
||||
assert_eq!(err.code(), &S3ErrorCode::AccessDenied);
|
||||
let err = router
|
||||
.call(req)
|
||||
.call_unsigned(http::Method::GET, uri)
|
||||
.await
|
||||
.expect_err("diagnostics handler must authenticate before inspecting local records");
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest.into());
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
|
||||
assert!(err.to_string().contains("authentication required"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,56 +91,42 @@ impl Operation for HealthCheckHandler {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http_body_util::BodyExt as _;
|
||||
use s3s::route::S3Route as _;
|
||||
use serial_test::serial;
|
||||
use temp_env::with_var;
|
||||
|
||||
fn health_request(method: Method) -> s3s::S3Request<s3s::Body> {
|
||||
s3s::S3Request {
|
||||
input: s3s::Body::empty(),
|
||||
method,
|
||||
uri: HEALTH_PREFIX.parse().expect("health URI"),
|
||||
headers: HeaderMap::new(),
|
||||
extensions: http::Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_real_health_handler(method: Method) -> s3s::S3Response<s3s::Body> {
|
||||
async fn call_real_health_handler(method: Method) -> (StatusCode, HeaderMap, Body) {
|
||||
let mut router = S3Router::new(false);
|
||||
router
|
||||
.insert(method.clone(), HEALTH_PREFIX, AdminOperation(&HealthCheckHandler {}))
|
||||
.expect("health route should register");
|
||||
router
|
||||
.call(health_request(method))
|
||||
.call_unsigned(method, HEALTH_PREFIX.parse().expect("health URI"))
|
||||
.await
|
||||
.expect("health handler should return a response")
|
||||
.into_parts()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn real_health_handler_get_and_head_preserve_wire_branches() {
|
||||
let get = call_real_health_handler(Method::GET).await;
|
||||
assert_eq!(get.status, Some(StatusCode::OK));
|
||||
let (get_status, get_headers, get_body) = call_real_health_handler(Method::GET).await;
|
||||
assert_eq!(get_status, StatusCode::OK);
|
||||
assert_eq!(
|
||||
get.headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()),
|
||||
get_headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()),
|
||||
Some("application/json")
|
||||
);
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&get.output.collect().await.expect("GET body should read").to_bytes())
|
||||
serde_json::from_slice(&get_body.collect().await.expect("GET body should read").to_bytes())
|
||||
.expect("GET health response should be JSON");
|
||||
assert_eq!(payload["status"], "ok");
|
||||
|
||||
let head = call_real_health_handler(Method::HEAD).await;
|
||||
assert_eq!(head.status, Some(StatusCode::OK));
|
||||
let (head_status, head_headers, head_body) = call_real_health_handler(Method::HEAD).await;
|
||||
assert_eq!(head_status, StatusCode::OK);
|
||||
assert_eq!(
|
||||
head.headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()),
|
||||
head_headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()),
|
||||
Some("application/json")
|
||||
);
|
||||
assert!(
|
||||
head.output
|
||||
head_body
|
||||
.collect()
|
||||
.await
|
||||
.expect("HEAD body should read")
|
||||
@@ -151,18 +137,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn real_health_handler_rejects_unsupported_methods_with_allow_header() {
|
||||
let response = call_real_health_handler(Method::POST).await;
|
||||
let (status, headers, body) = call_real_health_handler(Method::POST).await;
|
||||
|
||||
assert_eq!(response.status, Some(StatusCode::METHOD_NOT_ALLOWED));
|
||||
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
|
||||
assert_eq!(headers.get(http::header::ALLOW).and_then(|value| value.to_str().ok()), Some("GET, HEAD"));
|
||||
assert_eq!(
|
||||
response
|
||||
.headers
|
||||
.get(http::header::ALLOW)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("GET, HEAD")
|
||||
);
|
||||
assert_eq!(
|
||||
response.output.collect().await.expect("error body should read").to_bytes(),
|
||||
body.collect().await.expect("error body should read").to_bytes(),
|
||||
bytes::Bytes::from_static(b"Method Not Allowed")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1299,9 +1299,9 @@ mod pools_handler_tests {
|
||||
|
||||
let err = decommission_start_api_error(err);
|
||||
|
||||
assert_eq!(err.code, s3s::S3ErrorCode::ServiceUnavailable);
|
||||
assert_eq!(err.code, rustfs_s3_types::S3ErrorCode::ServiceUnavailable.into());
|
||||
assert!(err.message.contains("data movement gate (decommission activation)"), "{}", err.message);
|
||||
let err = s3s::S3Error::from(contextualize_admin_pool_api_error(err, "start decommission", "pools [0]"));
|
||||
let err = rustfs_s3_types::S3Error::from(contextualize_admin_pool_api_error(err, "start decommission", "pools [0]"));
|
||||
assert_eq!(
|
||||
err.headers().and_then(|headers| headers.get(http::header::RETRY_AFTER)),
|
||||
Some(&http::HeaderValue::from_static("1")),
|
||||
@@ -1313,8 +1313,8 @@ mod pools_handler_tests {
|
||||
fn test_decommission_start_api_error_reports_a_refused_preflight_heal_as_retryable() {
|
||||
let err = decommission_start_api_error(crate::storage_api::error::StorageError::SlowDown);
|
||||
|
||||
assert_eq!(err.code, s3s::S3ErrorCode::ServiceUnavailable);
|
||||
let err = s3s::S3Error::from(contextualize_admin_pool_api_error(err, "start decommission", "pools [0]"));
|
||||
assert_eq!(err.code, rustfs_s3_types::S3ErrorCode::ServiceUnavailable.into());
|
||||
let err = rustfs_s3_types::S3Error::from(contextualize_admin_pool_api_error(err, "start decommission", "pools [0]"));
|
||||
assert_eq!(err.status_code(), Some(http::StatusCode::SERVICE_UNAVAILABLE));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::admin::console::{is_console_path, make_console_server};
|
||||
use crate::admin::handlers::oidc::is_oidc_path;
|
||||
use crate::admin::operation::{AdminRequest, Operation as OwnedOperation};
|
||||
use crate::admin::runtime_sources::{
|
||||
ServerContextSlot, app_context_from_req, current_boot_time, current_bucket_monitor_handle, current_deployment_id,
|
||||
ServerContextSlot, app_context_from_extensions, current_boot_time, current_bucket_monitor_handle, current_deployment_id,
|
||||
current_notification_system, current_object_store_handle, current_region, current_replication_pool_handle,
|
||||
current_server_config, default_object_usecase,
|
||||
};
|
||||
@@ -3021,7 +3021,7 @@ async fn handle_replication_extension_request(
|
||||
match ext_req.route {
|
||||
ReplicationExtRoute::MetricsV1 | ReplicationExtRoute::MetricsV2 => {
|
||||
ensure_replication_config_exists(&ext_req.bucket).await?;
|
||||
build_replication_metrics_response(&ext_req.bucket, ext_req.route, app_context_from_req(req)).await
|
||||
build_replication_metrics_response(&ext_req.bucket, ext_req.route, app_context_from_extensions(&req.extensions)).await
|
||||
}
|
||||
ReplicationExtRoute::Check => {
|
||||
let (versioning, _) = metadata_sys::get_versioning_config(&ext_req.bucket)
|
||||
@@ -3233,6 +3233,51 @@ impl<T: Operation> S3Router<T> {
|
||||
pub(crate) fn registered_routes(&self) -> &[String] {
|
||||
&self.registered_routes
|
||||
}
|
||||
|
||||
/// Runs an unsigned request (no presented credentials, no headers, empty
|
||||
/// body) through this router's `check_access` gate, so handler tests can
|
||||
/// pin the router's authentication with owned types only.
|
||||
pub(crate) async fn check_unsigned_access(&self, method: Method, uri: Uri) -> rustfs_s3_types::S3Result<()> {
|
||||
let mut req = unsigned_legacy_request(method, uri);
|
||||
S3Route::check_access(self, &mut req)
|
||||
.await
|
||||
.map_err(rustfs_s3_types::S3Error::from)
|
||||
}
|
||||
|
||||
/// Dispatches an unsigned request (no presented credentials, no headers,
|
||||
/// empty body) through this router's `call`, returning the owned admin
|
||||
/// response. Every dispatched response carries a status, so a missing one
|
||||
/// fails the test.
|
||||
pub(crate) async fn call_unsigned(
|
||||
&self,
|
||||
method: Method,
|
||||
uri: Uri,
|
||||
) -> rustfs_s3_types::S3Result<crate::admin::operation::AdminResponse> {
|
||||
let resp = S3Route::call(self, unsigned_legacy_request(method, uri))
|
||||
.await
|
||||
.map_err(rustfs_s3_types::S3Error::from)?;
|
||||
let status = resp.status.expect("a dispatched admin response carries a status");
|
||||
Ok(crate::admin::operation::AdminResponse::with_headers(
|
||||
status,
|
||||
resp.output.into(),
|
||||
resp.headers,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn unsigned_legacy_request(method: Method, uri: Uri) -> S3Request<Body> {
|
||||
S3Request {
|
||||
input: Body::empty(),
|
||||
method,
|
||||
uri,
|
||||
headers: HeaderMap::new(),
|
||||
extensions: Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Operation> Default for S3Router<T> {
|
||||
|
||||
@@ -66,10 +66,6 @@ pub(crate) fn current_object_data_cache() -> Option<Arc<ObjectDataCacheAdapter>>
|
||||
current_object_data_cache_handle_for_context(context.as_deref())
|
||||
}
|
||||
|
||||
pub(crate) fn app_context_from_req<B>(req: &s3s::S3Request<B>) -> Option<Arc<AppContext>> {
|
||||
app_context_from_extensions(&req.extensions)
|
||||
}
|
||||
|
||||
pub(crate) fn app_context_from_envelope(envelope: &RequestEnvelope) -> Option<Arc<AppContext>> {
|
||||
match envelope.server_context() {
|
||||
Some(slot) => slot.installed_app_context(),
|
||||
@@ -160,17 +156,17 @@ 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, publish_test_app_context,
|
||||
current_app_context, object_store_from_envelope, publish_test_app_context,
|
||||
};
|
||||
use crate::admin::router::{Operation, S3Router};
|
||||
use crate::app::RequestEnvelope;
|
||||
use crate::admin::operation::{AdminRequest, AdminResponse, Operation};
|
||||
use crate::admin::router::S3Router;
|
||||
use crate::app::{RequestEnvelope, RequestHead};
|
||||
use hyper::{Method, StatusCode};
|
||||
use matchit::Params;
|
||||
use rustfs_iam::{store::object::ObjectStore, sys::IamSys};
|
||||
use rustfs_kms::KmsServiceManager;
|
||||
use rustfs_s3_types::Body;
|
||||
use rustfs_s3_types::{S3ErrorCode, s3_error};
|
||||
use s3s::route::S3Route;
|
||||
use s3s::{Body, S3Request, S3Response};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct UnreadyIam;
|
||||
@@ -213,22 +209,16 @@ mod tests {
|
||||
Arc::new(AppContext::new(context.object_store(), Arc::new(UnreadyIam), Arc::new(TestKms)))
|
||||
}
|
||||
|
||||
fn request(extensions: http::Extensions) -> S3Request<Body> {
|
||||
S3Request {
|
||||
input: Body::empty(),
|
||||
fn envelope(extensions: http::Extensions) -> RequestEnvelope {
|
||||
let head = RequestHead {
|
||||
method: Method::GET,
|
||||
uri: "/context-probe".parse().expect("test URI"),
|
||||
headers: http::HeaderMap::new(),
|
||||
extensions,
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn envelope(extensions: http::Extensions) -> RequestEnvelope {
|
||||
RequestEnvelope::from_s3s(&request(extensions))
|
||||
};
|
||||
RequestEnvelope::from_parts(head, extensions)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -240,8 +230,7 @@ mod tests {
|
||||
extensions.insert(slot);
|
||||
|
||||
assert!(app_context_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!(object_store_from_envelope(&envelope(extensions)).is_none());
|
||||
assert!(Arc::ptr_eq(
|
||||
¤t_app_context().expect("ambient context must remain available"),
|
||||
&ambient
|
||||
@@ -333,13 +322,10 @@ mod tests {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for ContextDependentAdminRoute {
|
||||
async fn call(
|
||||
&self,
|
||||
req: S3Request<Body>,
|
||||
_params: Params<'_, '_>,
|
||||
) -> rustfs_s3_types::S3Result<S3Response<(StatusCode, Body)>> {
|
||||
app_context_from_req(&req).ok_or_else(|| s3_error!(ServiceUnavailable, "server context is not ready"))?;
|
||||
Ok(S3Response::new((StatusCode::NO_CONTENT, Body::empty())))
|
||||
async fn call(&self, req: AdminRequest, _params: Params<'_, '_>) -> rustfs_s3_types::S3Result<AdminResponse> {
|
||||
app_context_from_envelope(req.envelope())
|
||||
.ok_or_else(|| s3_error!(ServiceUnavailable, "server context is not ready"))?;
|
||||
Ok(AdminResponse::new(StatusCode::NO_CONTENT, Body::empty()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,16 +341,16 @@ mod tests {
|
||||
.expect("register test route");
|
||||
|
||||
let err = router
|
||||
.call(request(http::Extensions::new()))
|
||||
.call_unsigned(Method::GET, "/context-probe".parse().expect("test URI"))
|
||||
.await
|
||||
.expect_err("empty slot must fail closed");
|
||||
assert_eq!(err.code(), &S3ErrorCode::ServiceUnavailable.into());
|
||||
assert_eq!(err.code(), &S3ErrorCode::ServiceUnavailable);
|
||||
|
||||
assert!(slot.install(distinct_context(&ambient)));
|
||||
let response = router
|
||||
.call(request(http::Extensions::new()))
|
||||
.call_unsigned(Method::GET, "/context-probe".parse().expect("test URI"))
|
||||
.await
|
||||
.expect("installed slot must serve request");
|
||||
assert_eq!(response.status, Some(StatusCode::NO_CONTENT));
|
||||
assert_eq!(response.into_parts().0, StatusCode::NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +165,10 @@ 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(s3s::Body::from(encrypted), 1024, "/minio/admin/v3/list-users", "secret")
|
||||
.await
|
||||
.expect("decode payload");
|
||||
let decoded =
|
||||
read_compatible_admin_body(rustfs_s3_types::Body::from(encrypted), 1024, "/minio/admin/v3/list-users", "secret")
|
||||
.await
|
||||
.expect("decode payload");
|
||||
|
||||
assert_eq!(decoded, payload);
|
||||
}
|
||||
@@ -191,12 +192,17 @@ mod tests {
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(err.message(), Some("failed to read request body: length limit exceeded"));
|
||||
|
||||
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")
|
||||
// A streaming body has no exact length up front, so the limit trips
|
||||
// while frames accumulate rather than on the size hint.
|
||||
let streaming_body = rustfs_s3_types::Body::from_stream(futures_util::stream::iter([
|
||||
Ok::<_, std::io::Error>(bytes::Bytes::from_static(b"nine ")),
|
||||
Ok(bytes::Bytes::from_static(b"bytes!")),
|
||||
]));
|
||||
let streaming_err = read_compatible_admin_body(streaming_body, 8, "/rustfs/admin/v3/list-users", "secret")
|
||||
.await
|
||||
.expect_err("legacy streaming bodies retain the same limit error");
|
||||
assert_eq!(legacy_err.code(), err.code());
|
||||
assert_eq!(legacy_err.message(), err.message());
|
||||
.expect_err("streaming bodies retain the same limit error");
|
||||
assert_eq!(streaming_err.code(), err.code());
|
||||
assert_eq!(streaming_err.message(), err.message());
|
||||
|
||||
let exact =
|
||||
read_compatible_admin_body(rustfs_s3_types::Body::from("12345678"), 8, "/rustfs/admin/v3/list-users", "secret")
|
||||
|
||||
Reference in New Issue
Block a user