mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 02:56:18 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70b1fb7d1a |
@@ -853,32 +853,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_error_conversions() {
|
||||
// A plain io::Error carries no typed payload to recover, so it lands in
|
||||
// `Io` rather than being guessed at from its kind — `NotFound` here must
|
||||
// not silently become `FileNotFound`, which quorum aggregation counts as
|
||||
// a different error (rustfs/backlog#1836).
|
||||
// Test From implementations
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
|
||||
let disk_error: DiskError = io_error.into();
|
||||
match &disk_error {
|
||||
DiskError::Io(inner) => assert_eq!(inner.kind(), std::io::ErrorKind::NotFound),
|
||||
other => panic!("a plain io::Error must stay typed as Io, got {other:?}"),
|
||||
}
|
||||
let _disk_error: DiskError = io_error.into();
|
||||
|
||||
// A typed DiskError boxed through io::Error round-trips back to itself
|
||||
// instead of degrading to `Io`.
|
||||
let boxed: std::io::Error = std::io::Error::other(DiskError::VolumeNotFound);
|
||||
assert_eq!(DiskError::from(boxed), DiskError::VolumeNotFound);
|
||||
|
||||
// serde_json errors have no dedicated variant and fold into `other`,
|
||||
// keeping the original message.
|
||||
let json_str = r#"{"invalid": json}"#;
|
||||
let json_str = r#"{"invalid": json}"#; // Invalid JSON
|
||||
let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
|
||||
let json_message = json_error.to_string();
|
||||
let disk_error: DiskError = json_error.into();
|
||||
assert!(
|
||||
disk_error.to_string().contains(&json_message),
|
||||
"the json error message must survive the conversion: {disk_error}"
|
||||
);
|
||||
let _disk_error: DiskError = json_error.into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -436,45 +436,30 @@ mod tests {
|
||||
assert_eq!(unknown_profile.sequential_boost_multiplier, 1.0);
|
||||
}
|
||||
|
||||
// What platform probing returns depends on the machine, so these pin the two
|
||||
// rules that do not: the override wins over probing, and probing that is
|
||||
// switched off reports Unknown rather than guessing (rustfs/backlog#1836).
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn storage_media_override_wins_over_platform_detection() {
|
||||
for (override_value, expected) in [
|
||||
("nvme", StorageMedia::Nvme),
|
||||
("ssd", StorageMedia::Ssd),
|
||||
("hdd", StorageMedia::Hdd),
|
||||
] {
|
||||
assert_eq!(detect_storage_media(true, override_value), expected);
|
||||
assert_eq!(
|
||||
detect_storage_media(false, override_value),
|
||||
expected,
|
||||
"an override must be honoured even with detection disabled"
|
||||
);
|
||||
fn test_linux_storage_detection_exists() {
|
||||
// This test just verifies the detection function exists and doesn't panic
|
||||
// The actual result depends on the system it's running on
|
||||
let result = detect_storage_media(true, "");
|
||||
// We should get some result (not panic)
|
||||
match result {
|
||||
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown => {
|
||||
// All valid results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn disabled_detection_reports_unknown_instead_of_guessing() {
|
||||
assert_eq!(detect_storage_media(false, ""), StorageMedia::Unknown);
|
||||
assert_eq!(
|
||||
detect_storage_media(false, "not-a-medium"),
|
||||
StorageMedia::Unknown,
|
||||
"an unparseable override falls through to the disabled path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_detection_returns_a_medium_for_this_platform() {
|
||||
// Whatever this machine reports, it must be one of the known variants and
|
||||
// it must be stable across calls — a probe that flapped would make the
|
||||
// scheduler's profile depend on when it asked.
|
||||
let first = detect_storage_media(true, "");
|
||||
assert!(matches!(
|
||||
first,
|
||||
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown
|
||||
));
|
||||
assert_eq!(detect_storage_media(true, ""), first);
|
||||
fn test_macos_storage_detection_exists() {
|
||||
// This test just verifies the detection function exists and doesn't panic
|
||||
let result = detect_storage_media(true, "");
|
||||
// We should get some result (not panic)
|
||||
match result {
|
||||
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown => {
|
||||
// All valid results
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,19 +527,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stopping_replay_workers_is_a_no_op_when_there_are_none() {
|
||||
async fn runtime_facade_stops_empty_replay_workers() {
|
||||
let (facade, _, _) = build_facade();
|
||||
|
||||
facade.stop_replay_workers().await;
|
||||
|
||||
// The stop path takes the worker list and hands it to the adapter, so an
|
||||
// empty facade must come back with the list still empty and dispatch
|
||||
// released rather than left paused (rustfs/backlog#1836).
|
||||
assert!(facade.replay_workers.read().await.is_empty());
|
||||
|
||||
// Calling it twice must stay harmless: shutdown paths do exactly that.
|
||||
facade.stop_replay_workers().await;
|
||||
assert!(facade.replay_workers.read().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -873,16 +873,9 @@ mod tests {
|
||||
/// now return a finite, non-panicking mask.
|
||||
#[test]
|
||||
fn test_mask_never_recurses_for_any_variant() {
|
||||
// Terminating is the point — a regression here overflows the stack rather
|
||||
// than failing an assertion — but the masks are collected and checked so
|
||||
// the loop cannot be optimised into nothing and so a variant that starts
|
||||
// returning an empty mask is caught too (rustfs/backlog#1836).
|
||||
let masks: Vec<u64> = ALL_EVENT_NAMES.iter().map(|ev| ev.mask()).collect();
|
||||
|
||||
assert_eq!(masks.len(), ALL_EVENT_NAMES.len());
|
||||
for (ev, mask) in ALL_EVENT_NAMES.iter().zip(&masks) {
|
||||
assert_ne!(*mask, 0, "{ev:?} must carry at least one bit");
|
||||
assert_eq!(ev.mask(), *mask, "{ev:?} must return the same mask every call");
|
||||
for ev in ALL_EVENT_NAMES {
|
||||
// Must terminate (no infinite recursion / stack overflow).
|
||||
let _ = ev.mask();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use crate::admin::storage_api::cluster::{CapabilityState, CapabilityStatus, ObservabilitySnapshot, TopologySnapshot};
|
||||
use crate::admin::{
|
||||
auth::validate_admin_request,
|
||||
auth::authorize_admin_request,
|
||||
router::{AdminOperation, Operation, S3Router},
|
||||
runtime_sources::default_admin_usecase,
|
||||
storage_api::cluster::{
|
||||
@@ -24,11 +24,10 @@ use crate::admin::{
|
||||
},
|
||||
system,
|
||||
};
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
use crate::cluster_snapshot::{
|
||||
ClusterReadOnlySnapshot, ClusterRuntimeReadinessState, ClusterRuntimeStatusSnapshot, cluster_has_actionable_pressure,
|
||||
};
|
||||
use crate::server::{ADMIN_PREFIX, ReadinessDegradedReason, RemoteAddr};
|
||||
use crate::server::{ADMIN_PREFIX, ReadinessDegradedReason};
|
||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use hyper::Method;
|
||||
use matchit::Params;
|
||||
@@ -66,23 +65,15 @@ pub(crate) struct ClusterSnapshotDiscoveryResponse {
|
||||
pub components: Option<ClusterComponentStatusView>,
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
let Some(input_cred) = &req.credentials else {
|
||||
if req.credentials.is_none() {
|
||||
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?;
|
||||
|
||||
validate_admin_request(
|
||||
&req.headers,
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
|
||||
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
||||
)
|
||||
.await
|
||||
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_json_response(
|
||||
@@ -953,6 +944,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// This endpoint authorizes through the shared admin gate, which reports
|
||||
/// "get cred failed" for a credential-less request. The pre-check keeps the
|
||||
/// 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 err = super::authorize_cluster_snapshot_request(&req)
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected");
|
||||
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(err.message(), Some("authentication required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_snapshot_response_serializes_none_snapshot() {
|
||||
let value = serde_json::to_value(ClusterSnapshotResponse { snapshot: None }).expect("serialize response");
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use crate::admin::storage_api::cluster::CapabilityStatus;
|
||||
use crate::admin::{
|
||||
auth::validate_admin_request,
|
||||
auth::authorize_admin_request,
|
||||
handlers::{cluster_snapshot, plugins_instances, system},
|
||||
plugin_contract::{
|
||||
PluginContractDomain, PluginInstanceDiagnosticCode, PluginInstanceDiagnosticCount, PluginInstanceEntry,
|
||||
@@ -22,8 +22,7 @@ use crate::admin::{
|
||||
},
|
||||
router::{AdminOperation, Operation, S3Router},
|
||||
};
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
use crate::server::{ADMIN_PREFIX, RemoteAddr};
|
||||
use crate::server::ADMIN_PREFIX;
|
||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use hyper::Method;
|
||||
use matchit::Params;
|
||||
@@ -183,42 +182,26 @@ fn map_extension_instance(instance: PluginInstanceEntry) -> ExtensionInstanceEnt
|
||||
}
|
||||
}
|
||||
|
||||
/// The pre-check keeps this endpoint's historical missing-credentials message;
|
||||
/// the shared gate reports "get cred failed".
|
||||
async fn authorize_extension_catalog_request(req: &S3Request<Body>) -> S3Result<()> {
|
||||
let Some(input_cred) = &req.credentials else {
|
||||
if req.credentials.is_none() {
|
||||
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?;
|
||||
|
||||
validate_admin_request(
|
||||
&req.headers,
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
|
||||
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
||||
)
|
||||
.await
|
||||
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The pre-check keeps this endpoint's historical missing-credentials message;
|
||||
/// the shared gate reports "get cred failed".
|
||||
async fn authorize_extension_instance_request(req: &S3Request<Body>) -> S3Result<()> {
|
||||
let Some(input_cred) = &req.credentials else {
|
||||
if req.credentials.is_none() {
|
||||
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?;
|
||||
|
||||
validate_admin_request(
|
||||
&req.headers,
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::GetBucketTargetAction)],
|
||||
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
||||
)
|
||||
.await
|
||||
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::GetBucketTargetAction)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_json_response(
|
||||
@@ -320,6 +303,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Both extension gates authorize through the shared admin gate, which reports
|
||||
/// "get cred failed" for a credential-less request. The pre-check keeps the
|
||||
/// message these endpoints have always returned (rustfs/backlog#1829).
|
||||
#[tokio::test]
|
||||
async fn extension_gates_keep_their_missing_credentials_message() {
|
||||
let credential_less_request = || s3s::S3Request {
|
||||
input: s3s::Body::from(String::new()),
|
||||
method: http::Method::GET,
|
||||
uri: http::Uri::from_static("/rustfs/admin/v4/extensions/catalog"),
|
||||
headers: http::HeaderMap::new(),
|
||||
extensions: http::Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
|
||||
for err in [
|
||||
super::authorize_extension_catalog_request(&credential_less_request())
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected"),
|
||||
super::authorize_extension_instance_request(&credential_less_request())
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected"),
|
||||
] {
|
||||
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(err.message(), Some("authentication required"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_ops_schemas_register_cleanly_in_runtime_registries() {
|
||||
let mut diagnostics_registry = rustfs_targets::OpsDiagnosticsRegistry::new();
|
||||
|
||||
@@ -21,12 +21,11 @@
|
||||
//! that bucket, and with `bucket`+`object` it flushes that one identity — the
|
||||
//! only remediation for a poisoned entry short of a node restart.
|
||||
|
||||
use crate::admin::auth::validate_admin_request;
|
||||
use crate::admin::auth::authorize_admin_request;
|
||||
use crate::admin::router::{AdminOperation, Operation, S3Router};
|
||||
use crate::admin::runtime_sources::current_object_data_cache;
|
||||
use crate::app::object_data_cache::ObjectDataCacheAdapter;
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
use crate::server::{ADMIN_PREFIX, RemoteAddr};
|
||||
use crate::server::ADMIN_PREFIX;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use hyper::{Method, StatusCode};
|
||||
use matchit::Params;
|
||||
@@ -76,17 +75,14 @@ pub fn register_object_data_cache_route(r: &mut S3Router<AdminOperation>) -> std
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The pre-check keeps these endpoints' historical missing-credentials message;
|
||||
/// the shared gate reports "get cred failed".
|
||||
async fn authorize(req: &S3Request<Body>, action: AdminAction) -> S3Result<()> {
|
||||
let Some(input_cred) = req.credentials.as_ref() else {
|
||||
if req.credentials.is_none() {
|
||||
return Err(s3_error!(InvalidRequest, "missing credentials"));
|
||||
};
|
||||
let (cred, owner) =
|
||||
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
|
||||
let remote_addr = req
|
||||
.extensions
|
||||
.get::<Option<RemoteAddr>>()
|
||||
.and_then(|opt| opt.map(|addr| addr.0));
|
||||
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await
|
||||
}
|
||||
authorize_admin_request(req, vec![Action::AdminAction(action)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn json_response<T: Serialize>(body: &T) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
@@ -208,6 +204,30 @@ mod tests {
|
||||
assert_eq!(invalidation_outcome(&ObjectDataCacheInvalidationResult::NoOp), ("noop", 0));
|
||||
}
|
||||
|
||||
/// 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 authorize_keeps_its_missing_credentials_message() {
|
||||
let req = S3Request {
|
||||
input: Body::from(String::new()),
|
||||
method: Method::GET,
|
||||
uri: "/rustfs/admin/v3/object-data-cache/stats".parse().expect("uri should parse"),
|
||||
headers: HeaderMap::new(),
|
||||
extensions: http::Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
|
||||
let err = authorize(&req, AdminAction::ServerInfoAdminAction)
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected");
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(err.message(), Some("missing credentials"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_handler_requires_server_info_action() {
|
||||
// Guard the auth contract: the stats endpoint is a read, the flush
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::admin::{
|
||||
auth::validate_admin_request,
|
||||
auth::authorize_admin_request,
|
||||
plugin_contract::{
|
||||
PluginCatalogAdminDiscovery, PluginCatalogDomainEntry, PluginCatalogEntry, PluginCatalogResponse, PluginContractDomain,
|
||||
PluginContractEntrypointKind, PluginContractPackaging, PluginDistributionContract, PluginRuntimeContract,
|
||||
@@ -21,8 +21,7 @@ use crate::admin::{
|
||||
router::{AdminOperation, Operation, S3Router},
|
||||
runtime_sources::default_admin_usecase,
|
||||
};
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
use crate::server::{ADMIN_PREFIX, RemoteAddr};
|
||||
use crate::server::ADMIN_PREFIX;
|
||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use hyper::Method;
|
||||
use matchit::Params;
|
||||
@@ -114,23 +113,15 @@ fn merge_catalog_descriptor(plugins: &mut HashMap<&'static str, PluginCatalogEnt
|
||||
}
|
||||
}
|
||||
|
||||
/// The pre-check keeps this endpoint's historical missing-credentials message;
|
||||
/// the shared gate reports "get cred failed".
|
||||
async fn authorize_plugin_catalog_request(req: &S3Request<Body>) -> S3Result<()> {
|
||||
let Some(input_cred) = &req.credentials else {
|
||||
if req.credentials.is_none() {
|
||||
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?;
|
||||
|
||||
validate_admin_request(
|
||||
&req.headers,
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
|
||||
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
||||
)
|
||||
.await
|
||||
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_json_response(
|
||||
@@ -175,6 +166,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// This endpoint authorizes through the shared admin gate, which reports
|
||||
/// "get cred failed" for a credential-less request. The pre-check keeps the
|
||||
/// message it has always returned (rustfs/backlog#1829).
|
||||
#[tokio::test]
|
||||
async fn plugin_catalog_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/plugins/catalog"),
|
||||
headers: http::HeaderMap::new(),
|
||||
extensions: http::Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
|
||||
let err = super::authorize_plugin_catalog_request(&req)
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected");
|
||||
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(err.message(), Some("authentication required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_catalog_contains_representative_builtin_targets() {
|
||||
let response = build_catalog_response();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::admin::{
|
||||
auth::validate_admin_request,
|
||||
auth::authorize_admin_request,
|
||||
handlers::audit_runtime_config::{load_server_config_from_store, remove_audit_target_config, set_audit_target_config},
|
||||
handlers::notify_runtime_access::{
|
||||
load_notification_config_snapshot, remove_notification_target_config, set_notification_target_config,
|
||||
@@ -29,10 +29,9 @@ use crate::admin::{
|
||||
},
|
||||
router::{AdminOperation, Operation, S3Router},
|
||||
};
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
use crate::server::{
|
||||
ADMIN_PREFIX, RemoteAddr, is_audit_module_enabled, is_notify_module_enabled, refresh_audit_module_enabled,
|
||||
refresh_notify_module_enabled, refresh_persisted_module_switches_from_store,
|
||||
ADMIN_PREFIX, is_audit_module_enabled, is_notify_module_enabled, refresh_audit_module_enabled, refresh_notify_module_enabled,
|
||||
refresh_persisted_module_switches_from_store,
|
||||
};
|
||||
use hyper::{Method, StatusCode};
|
||||
use matchit::Params;
|
||||
@@ -563,42 +562,26 @@ fn plugin_instance_matches_query(instance: &PluginInstanceEntry, query: &str) ->
|
||||
.any(|field| field.to_ascii_lowercase().contains(&query))
|
||||
}
|
||||
|
||||
/// The pre-check keeps this endpoint's historical missing-credentials message;
|
||||
/// the shared gate reports "get cred failed".
|
||||
async fn authorize_plugin_instance_request(req: &S3Request<Body>) -> S3Result<()> {
|
||||
let Some(input_cred) = &req.credentials else {
|
||||
if req.credentials.is_none() {
|
||||
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?;
|
||||
|
||||
validate_admin_request(
|
||||
&req.headers,
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::GetBucketTargetAction)],
|
||||
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
||||
)
|
||||
.await
|
||||
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::GetBucketTargetAction)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The pre-check keeps this endpoint's historical missing-credentials message;
|
||||
/// the shared gate reports "get cred failed".
|
||||
async fn authorize_plugin_instance_write_request(req: &S3Request<Body>) -> S3Result<()> {
|
||||
let Some(input_cred) = &req.credentials else {
|
||||
if req.credentials.is_none() {
|
||||
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?;
|
||||
|
||||
validate_admin_request(
|
||||
&req.headers,
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::SetBucketTargetAction)],
|
||||
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
||||
)
|
||||
.await
|
||||
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::SetBucketTargetAction)]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn plugin_instance_mutation_block_reason(
|
||||
@@ -942,6 +925,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Both instance gates authorize through the shared admin gate, which reports
|
||||
/// "get cred failed" for a credential-less request. The pre-check keeps the
|
||||
/// message these endpoints have always returned (rustfs/backlog#1829).
|
||||
#[tokio::test]
|
||||
async fn plugin_instance_gates_keep_their_missing_credentials_message() {
|
||||
let credential_less_request = || S3Request {
|
||||
input: Body::from(String::new()),
|
||||
method: Method::GET,
|
||||
uri: Uri::from_static("/rustfs/admin/v4/plugins/instances"),
|
||||
headers: HeaderMap::new(),
|
||||
extensions: Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
|
||||
for err in [
|
||||
super::authorize_plugin_instance_request(&credential_less_request())
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected"),
|
||||
super::authorize_plugin_instance_write_request(&credential_less_request())
|
||||
.await
|
||||
.expect_err("a request without credentials must be rejected"),
|
||||
] {
|
||||
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(err.message(), Some("authentication required"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_instance_without_runtime_appears_offline() {
|
||||
let config = Config(HashMap::from([(
|
||||
|
||||
@@ -49,47 +49,19 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
VERIFY_SIGNALS = re.compile(
|
||||
r"assert[a-z0-9_]*!|debug_assert|panic!\(|\.expect\(|\.unwrap\(|"
|
||||
r"assert!|assert_eq!|assert_ne!|debug_assert|panic!\(|\.expect\(|\.unwrap\(|"
|
||||
r"unreachable!|matches!\(|insta::|proptest!|\.await\?|\)\?|\?;|should_panic"
|
||||
)
|
||||
DELEGATION = re.compile(
|
||||
r"\b(?:assert|verify|check|expect|ensure|run)_[a-z0-9_]*(?:::<[^>]*>)?\s*\(|"
|
||||
r"\b[a-z0-9_]+_(?:case|cases|harness|roundtrip|round_trip)(?:::<[^>]*>)?\s*\("
|
||||
r"\b(?:assert|verify|check|expect|ensure|run)_[a-z0-9_]*\s*\(|"
|
||||
r"\b[a-z0-9_]+_(?:case|cases|harness|roundtrip|round_trip)\s*\("
|
||||
)
|
||||
|
||||
# A body whose whole content is one call delegates by construction, whatever the
|
||||
# callee is named: `run(DurabilityMode::Strict).await` and
|
||||
# `aborting_encode_drops_blocked_producer(EncodePipeline::Vec).await` both hand
|
||||
# every assertion to a shared harness.
|
||||
SINGLE_CALL_BODY = re.compile(
|
||||
r"\A\s*[a-zA-Z_][a-zA-Z0-9_:]*(?:::<[^>]*>)?\s*\([^;]*\)\s*(?:\.await\s*)?;?\s*\Z",
|
||||
re.S,
|
||||
)
|
||||
|
||||
# A nested `fn` that is only bound and discarded is a signature guard: the type
|
||||
# system is the assertion, exactly like the `fn _name()` form below.
|
||||
SIGNATURE_GUARD = re.compile(r"\bfn\s+[a-zA-Z0-9_]+\s*(?:<[^>]*>)?\s*\([^;]*\)[^;]*\{", re.S)
|
||||
DISCARDED_BINDING = re.compile(r"\blet\s+_\s*=\s*[a-zA-Z_][a-zA-Z0-9_]*\s*;")
|
||||
# `let _ = Type::<T>::method;` — a path item referenced but never called can only
|
||||
# be a signature guard; the call form (`let _ = x.foo();`) is excluded by the
|
||||
# absence of parens before the semicolon.
|
||||
DISCARDED_PATH_ITEM = re.compile(r"\blet\s+_\s*=\s*[a-zA-Z_][a-zA-Z0-9_]*(?:::(?:<[^>]*>|[a-zA-Z_][a-zA-Z0-9_]*))+\s*;")
|
||||
COMPILE_TIME_CHECK = re.compile(r"\bfn\s+_[a-zA-Z0-9_]*\s*(?:<[^>]*>)?\s*\(")
|
||||
TEST_ATTR = re.compile(r"#\[(?:tokio::)?test[\](]")
|
||||
TEST_CASE_ATTR = re.compile(r"#\[test_case")
|
||||
FN_LINE = re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)")
|
||||
|
||||
|
||||
|
||||
def extract_body(text: str) -> str:
|
||||
"""Return what is between the outermost braces of a scanned function."""
|
||||
start = text.find("{")
|
||||
end = text.rfind("}")
|
||||
if start == -1 or end <= start:
|
||||
return text
|
||||
return text[start + 1 : end]
|
||||
|
||||
|
||||
def scan_file(path: Path):
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").split("\n")
|
||||
@@ -133,17 +105,7 @@ def scan_file(path: Path):
|
||||
break
|
||||
k += 1
|
||||
text = "\n".join(body)
|
||||
# The attribute block carries verification too: `#[should_panic(expected
|
||||
# = "...")]` makes the panic message the assertion.
|
||||
attr_text = "\n".join(attrs)
|
||||
inner = extract_body(text)
|
||||
delegates = (
|
||||
DELEGATION.search(text)
|
||||
or SINGLE_CALL_BODY.match(inner)
|
||||
or (SIGNATURE_GUARD.search(inner) and DISCARDED_BINDING.search(inner))
|
||||
or DISCARDED_PATH_ITEM.search(inner)
|
||||
)
|
||||
if not VERIFY_SIGNALS.search(text) and not VERIFY_SIGNALS.search(attr_text) and not delegates and not COMPILE_TIME_CHECK.search(text):
|
||||
if not VERIFY_SIGNALS.search(text) and not DELEGATION.search(text) and not COMPILE_TIME_CHECK.search(text):
|
||||
print(f"{path}:{j + 1}: {name}")
|
||||
i = k + 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user