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