mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-14 13:48:44 +00:00
refactor(storage): read and install authorization state on both edges
The state an access hook hands to a use case is now read and installed through the `AuthorizationRequest` trait. Its two implementors are the s3s request and `RequestEnvelope`, so the legacy hooks and the gateway edge call the same functions: - These readers are now generic over `AuthorizationRequest`: `apply_bucket_generation_guard`, `apply_copy_source_bucket_generation_guard` (both on one shared fence), `odm_read_generation`, `prepare_odm_read_generation`, `bucket_config_mutation_incarnation`, `replication_request_authorized`, `retained_table_data_plane_publication_guards`, `request_context_from_req`, `resource_free_condition_values`, `req_info_ref`, `req_info_mut`, `load_bucket_generation_from_store` and `SseKmsPrincipal::from_request`. - `load_bucket_generation` and `capture_odm_read_generation` now take the server context explicitly. The legacy hooks pass `FS::server_ctx`. - New trait accessors: request context; the bucket, pending-delete, copy-source and ODM generation guards; the bucket-config mutation snapshot. - New trait installers: the bucket, pending-delete and ODM generation guards. The legacy hooks now install those guards through them. - New: `authorize_object_request(req, bucket, key, version_id, action)`, the object twin of `authorize_bucket_request`, with no version swap. `versioned_read_action` and `versioned_tagging_action` become crate-visible so callers can pick the action first. The GetObjectAcl, PutObjectAcl, GetObjectLegalHold and GetObjectRetention hooks now call it. - Removed: `request_context_from_extensions`, which had no caller left. Each reader keeps the same answers and the same error codes and messages. Not built here: the bucket-configuration mutation shape, the body-input newtypes, and the replication-only, write-ETag and staged-part checks. Tests: `storage/access_readers_test.rs` covers each reader through both forms with the state present and absent (12 refusal cases, 11 accepted or absent ones), plus the installers on the envelope. The existing owned-versus-legacy decision table now also runs every case through `authorize_object_request` on the envelope.
This commit is contained in:
@@ -480,6 +480,37 @@ impl AuthorizationRequest for RequestEnvelope {
|
||||
fn req_info_mut(&mut self) -> Option<&mut ReqInfo> {
|
||||
self.req_info_mut()
|
||||
}
|
||||
fn request_context(&self) -> Option<&RequestContext> {
|
||||
self.request_context.as_ref()
|
||||
}
|
||||
fn bucket_generation(&self) -> Option<&BucketGenerationGuard> {
|
||||
self.bucket_generation.as_ref()
|
||||
}
|
||||
fn install_bucket_generation(&mut self, guard: BucketGenerationGuard) {
|
||||
self.bucket_generation = Some(guard);
|
||||
}
|
||||
fn pending_delete_bucket_generation(&self) -> Option<&PendingDeleteBucketGenerationGuard> {
|
||||
self.pending_delete_bucket_generation.as_ref()
|
||||
}
|
||||
fn install_pending_delete_bucket_generation(&mut self, guard: PendingDeleteBucketGenerationGuard) {
|
||||
self.pending_delete_bucket_generation = Some(guard);
|
||||
}
|
||||
fn copy_source_bucket_generation(&self) -> Option<&CopySourceBucketGenerationGuard> {
|
||||
self.copy_source_bucket_generation.as_ref()
|
||||
}
|
||||
fn odm_read_generation(&self) -> Option<&OdmReadGenerationGuard> {
|
||||
self.odm_read_generation.as_ref()
|
||||
}
|
||||
fn install_odm_read_generation(&mut self, guard: OdmReadGenerationGuard) {
|
||||
self.odm_read_generation = Some(guard);
|
||||
}
|
||||
fn bucket_config_mutation(&self) -> Option<&BucketConfigMutationSnapshot> {
|
||||
self.bucket_config_mutation.as_ref()
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn test_extensions(&self) -> &Extensions {
|
||||
&self.residual_extensions
|
||||
}
|
||||
fn internal_object_authorization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -970,14 +970,14 @@ pub(super) enum PutObjectOrigin<'a> {
|
||||
impl PutObjectOrigin<'_> {
|
||||
fn replication_request_authorized(&self) -> bool {
|
||||
match self {
|
||||
Self::S3 { req, .. } => replication_request_authorized(req),
|
||||
Self::S3 { req, .. } => replication_request_authorized(*req),
|
||||
Self::Internal { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_bucket_generation_guard(&self, bucket: &str, opts: &mut ObjectOptions) -> S3Result<()> {
|
||||
match self {
|
||||
Self::S3 { req, .. } => apply_bucket_generation_guard(req, bucket, opts),
|
||||
Self::S3 { req, .. } => apply_bucket_generation_guard(*req, bucket, opts),
|
||||
Self::Internal {
|
||||
expected_bucket_incarnation_id,
|
||||
..
|
||||
@@ -1010,14 +1010,14 @@ impl PutObjectOrigin<'_> {
|
||||
|
||||
fn table_data_plane_publication_guards(&self) -> Option<TableDataPlanePublicationGuards> {
|
||||
match self {
|
||||
Self::S3 { req, .. } => retained_table_data_plane_publication_guards(req),
|
||||
Self::S3 { req, .. } => retained_table_data_plane_publication_guards(*req),
|
||||
Self::Internal { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sse_principal(&self) -> Option<SseKmsPrincipal> {
|
||||
match self {
|
||||
Self::S3 { req, .. } => SseKmsPrincipal::from_request(req),
|
||||
Self::S3 { req, .. } => SseKmsPrincipal::from_request(*req),
|
||||
Self::Internal { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,9 +1536,19 @@ async fn owned_and_legacy_entry_points_reach_the_same_decisions() {
|
||||
});
|
||||
(case.facts)(&mut legacy);
|
||||
let mut owned = RequestEnvelope::from_s3s(&legacy);
|
||||
// The object shape both edges' access hooks call: it must reach the same decision.
|
||||
let mut owned_shape = RequestEnvelope::from_s3s(&legacy);
|
||||
let action = Action::S3Action(case.action);
|
||||
let legacy_result = crate::app::storage_api::owned_parity::authorize_request(&mut legacy, action).await;
|
||||
let owned_result = crate::app::authorize_owned_request(&mut owned, action, None).await;
|
||||
let shape_result = crate::app::storage_api::owned_parity::authorize_object_request(
|
||||
&mut owned_shape,
|
||||
case.bucket,
|
||||
case.object,
|
||||
None,
|
||||
action,
|
||||
)
|
||||
.await;
|
||||
let legacy_ok = match &legacy_result {
|
||||
Ok(()) => case.allowed,
|
||||
Err(err) => !case.allowed && err.code() == &S3ErrorCode::AccessDenied,
|
||||
@@ -1547,9 +1557,13 @@ async fn owned_and_legacy_entry_points_reach_the_same_decisions() {
|
||||
Ok(()) => case.allowed,
|
||||
Err(err) => !case.allowed && err.code() == &rustfs_s3_types::S3ErrorCode::AccessDenied,
|
||||
};
|
||||
if !legacy_ok || !owned_ok {
|
||||
let shape_ok = match &shape_result {
|
||||
Ok(()) => case.allowed,
|
||||
Err(err) => !case.allowed && err.code() == &S3ErrorCode::AccessDenied,
|
||||
};
|
||||
if !legacy_ok || !owned_ok || !shape_ok {
|
||||
mismatches.push(format!(
|
||||
"{} (expected allowed={}): legacy {legacy_result:?}, owned {owned_result:?}",
|
||||
"{} (expected allowed={}): legacy {legacy_result:?}, owned {owned_result:?}, object shape {shape_result:?}",
|
||||
case.name, case.allowed
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1848,7 +1848,8 @@ pub(crate) mod owned_parity {
|
||||
DenialAuditor, DenialRateLimiter, DenialScope, FS, ObjectTagConditions, PostObjectPolicyHeaders, PostObjectRequestMarker,
|
||||
ReqInfo, RequestContext, RequestedCopySource, RequestedObjectLegalHold, RequestedObjectLockRetainUntil,
|
||||
RequestedObjectLockRetention, RequestedObjectTags, SUPPRESSED_DENIALS_TAG, TableDataPlanePublicationGuards,
|
||||
authorization_denial_entry, authorization_denial_entry_for_view, authorize_request, parity_test_support,
|
||||
authorization_denial_entry, authorization_denial_entry_for_view, authorize_object_request, authorize_request,
|
||||
parity_test_support,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+221
-138
@@ -74,6 +74,9 @@ use url::Url;
|
||||
/// state this module keeps private.
|
||||
#[path = "access_owned/mod.rs"]
|
||||
pub(crate) mod access_owned;
|
||||
#[cfg(test)]
|
||||
#[path = "access_readers_test.rs"]
|
||||
mod readers_test;
|
||||
|
||||
fn iam_authorization_error(err: IamError) -> ApiError {
|
||||
match err {
|
||||
@@ -107,10 +110,8 @@ pub(crate) struct ReqInfo {
|
||||
pub suppress_denial_audit: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn replication_request_authorized<T>(req: &S3Request<T>) -> bool {
|
||||
req.extensions
|
||||
.get::<ReqInfo>()
|
||||
.is_some_and(|req_info| req_info.replication_request_authorized)
|
||||
pub(crate) fn replication_request_authorized<R: AuthorizationRequest + ?Sized>(req: &R) -> bool {
|
||||
req.req_info().is_some_and(|req_info| req_info.replication_request_authorized)
|
||||
}
|
||||
|
||||
fn has_replication_only_put_headers(headers: &HeaderMap) -> bool {
|
||||
@@ -437,9 +438,12 @@ pub(crate) struct BucketConfigMutationSnapshot {
|
||||
incarnation_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
pub(crate) fn bucket_config_mutation_incarnation<T>(req: &S3Request<T>, bucket: &str) -> S3Result<Option<uuid::Uuid>> {
|
||||
let Some(snapshot) = req.extensions.get::<BucketConfigMutationSnapshot>() else {
|
||||
if req.extensions.get::<std::sync::Arc<ServerContextSlot>>().is_some() {
|
||||
pub(crate) fn bucket_config_mutation_incarnation<R: AuthorizationRequest + ?Sized>(
|
||||
req: &R,
|
||||
bucket: &str,
|
||||
) -> S3Result<Option<uuid::Uuid>> {
|
||||
let Some(snapshot) = req.bucket_config_mutation() else {
|
||||
if req.server_context().is_some() {
|
||||
return Err(s3_error!(InternalError, "bucket config mutation snapshot is missing"));
|
||||
}
|
||||
return Ok(None);
|
||||
@@ -561,12 +565,12 @@ impl PendingDeleteBucketGenerationGuard {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn load_bucket_generation_from_store<T>(
|
||||
pub(crate) async fn load_bucket_generation_from_store<R: AuthorizationRequest + ?Sized>(
|
||||
store: &crate::storage::storage_api::ECStore,
|
||||
req: &S3Request<T>,
|
||||
req: &R,
|
||||
bucket: &str,
|
||||
) -> S3Result<BucketGenerationGuard> {
|
||||
if let Some(existing) = req.extensions.get::<BucketGenerationGuard>() {
|
||||
if let Some(existing) = req.bucket_generation() {
|
||||
return if existing.bucket == bucket {
|
||||
Ok(existing.clone())
|
||||
} else {
|
||||
@@ -581,9 +585,15 @@ pub(crate) async fn load_bucket_generation_from_store<T>(
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_bucket_generation<T>(fs: &FS, req: &S3Request<T>, bucket: &str) -> S3Result<BucketGenerationGuard> {
|
||||
let store = fs
|
||||
.server_ctx()
|
||||
/// Loads the bucket incarnation through `server_ctx`'s object store, or returns the guard the
|
||||
/// request already carries. The legacy hooks pass `FS::server_ctx`; the gateway edge passes the
|
||||
/// envelope's server context.
|
||||
pub(crate) async fn load_bucket_generation<R: AuthorizationRequest + ?Sized>(
|
||||
server_ctx: &ServerContextSlot,
|
||||
req: &R,
|
||||
bucket: &str,
|
||||
) -> S3Result<BucketGenerationGuard> {
|
||||
let store = server_ctx
|
||||
.object_store()
|
||||
.ok_or_else(|| s3_error!(InternalError, "object store is not initialized"))?;
|
||||
load_bucket_generation_from_store(store.as_ref(), req, bucket).await
|
||||
@@ -615,33 +625,39 @@ fn odm_read_source_configured(bucket: &str) -> bool {
|
||||
sys.is_module_enabled() && sys.state(bucket).is_some()
|
||||
}
|
||||
|
||||
async fn capture_odm_read_generation<T>(fs: &FS, req: &S3Request<T>, bucket: &str) -> OdmReadGenerationGuard {
|
||||
/// The on-demand-migration source generation a read may consult, captured before its
|
||||
/// authorization. A capture failure is kept in the guard and surfaces only on a source miss.
|
||||
pub(crate) async fn capture_odm_read_generation<R: AuthorizationRequest + ?Sized>(
|
||||
server_ctx: &ServerContextSlot,
|
||||
req: &R,
|
||||
bucket: &str,
|
||||
) -> OdmReadGenerationGuard {
|
||||
if !odm_read_source_configured(bucket) {
|
||||
return OdmReadGenerationGuard::Unavailable;
|
||||
}
|
||||
OdmReadGenerationGuard::from_result(load_bucket_generation(fs, req, bucket).await)
|
||||
OdmReadGenerationGuard::from_result(load_bucket_generation(server_ctx, req, bucket).await)
|
||||
}
|
||||
|
||||
/// Direct usecase callers have no access middleware; capture at their entry.
|
||||
/// A server request without the access marker must never bind to a later source.
|
||||
pub(crate) async fn prepare_odm_read_generation<T>(
|
||||
pub(crate) async fn prepare_odm_read_generation<R: AuthorizationRequest + ?Sized>(
|
||||
store: &crate::storage::storage_api::ECStore,
|
||||
req: &mut S3Request<T>,
|
||||
req: &mut R,
|
||||
bucket: &str,
|
||||
) {
|
||||
if req.extensions.get::<OdmReadGenerationGuard>().is_some() {
|
||||
if req.odm_read_generation().is_some() {
|
||||
return;
|
||||
}
|
||||
let guard = if req.extensions.get::<std::sync::Arc<ServerContextSlot>>().is_some() || !odm_read_source_configured(bucket) {
|
||||
let guard = if req.server_context().is_some() || !odm_read_source_configured(bucket) {
|
||||
OdmReadGenerationGuard::Unavailable
|
||||
} else {
|
||||
OdmReadGenerationGuard::from_result(load_bucket_generation_from_store(store, req, bucket).await)
|
||||
};
|
||||
req.extensions.insert(guard);
|
||||
req.install_odm_read_generation(guard);
|
||||
}
|
||||
|
||||
pub(crate) fn odm_read_generation<T>(req: &S3Request<T>, bucket: &str) -> S3Result<Option<uuid::Uuid>> {
|
||||
match req.extensions.get::<OdmReadGenerationGuard>() {
|
||||
pub(crate) fn odm_read_generation<R: AuthorizationRequest + ?Sized>(req: &R, bucket: &str) -> S3Result<Option<uuid::Uuid>> {
|
||||
match req.odm_read_generation() {
|
||||
Some(OdmReadGenerationGuard::Ready(guard)) if guard.bucket == bucket => Ok(Some(guard.incarnation_id)),
|
||||
Some(OdmReadGenerationGuard::Ready(_)) => {
|
||||
Err(s3_error!(InternalError, "source generation guard does not match request bucket"))
|
||||
@@ -663,60 +679,72 @@ async fn load_copy_source_bucket_generation(fs: &FS, bucket: &str) -> S3Result<C
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_bucket_generation_guard<T>(req: &S3Request<T>, bucket: &str, opts: &mut ObjectOptions) -> S3Result<()> {
|
||||
let pending_guard = req.extensions.get::<PendingDeleteBucketGenerationGuard>();
|
||||
let guard = match pending_guard {
|
||||
pub(crate) fn apply_bucket_generation_guard<R: AuthorizationRequest + ?Sized>(
|
||||
req: &R,
|
||||
bucket: &str,
|
||||
opts: &mut ObjectOptions,
|
||||
) -> S3Result<()> {
|
||||
let guard = match req.pending_delete_bucket_generation() {
|
||||
Some(PendingDeleteBucketGenerationGuard::Ready(guard)) => Some(guard),
|
||||
Some(PendingDeleteBucketGenerationGuard::Failed { code, message }) => {
|
||||
return Err(S3Error::with_message(code.clone(), message.clone()));
|
||||
}
|
||||
None => req.extensions.get::<BucketGenerationGuard>(),
|
||||
None => req.bucket_generation(),
|
||||
};
|
||||
let Some(guard) = guard else {
|
||||
if req.extensions.get::<std::sync::Arc<ServerContextSlot>>().is_some() {
|
||||
return Err(s3_error!(InternalError, "bucket generation guard is missing"));
|
||||
}
|
||||
if let Some(publication_guards) = req.extensions.get::<TableDataPlanePublicationGuards>() {
|
||||
publication_guards.add_lock_loss_fences(opts);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if guard.bucket != bucket {
|
||||
return Err(s3_error!(InternalError, "bucket generation guard does not match request bucket"));
|
||||
}
|
||||
opts.expected_bucket_incarnation_id = Some(guard.incarnation_id);
|
||||
if let Some(publication_guards) = req.extensions.get::<TableDataPlanePublicationGuards>() {
|
||||
publication_guards.add_lock_loss_fences(opts);
|
||||
}
|
||||
Ok(())
|
||||
apply_generation_fence(
|
||||
req,
|
||||
guard.map(|guard| (guard.bucket.as_str(), guard.incarnation_id)),
|
||||
bucket,
|
||||
opts,
|
||||
"bucket generation guard",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn retained_table_data_plane_publication_guards<T>(req: &S3Request<T>) -> Option<TableDataPlanePublicationGuards> {
|
||||
req.extensions.get::<TableDataPlanePublicationGuards>().cloned()
|
||||
pub(crate) fn retained_table_data_plane_publication_guards<R: AuthorizationRequest + ?Sized>(
|
||||
req: &R,
|
||||
) -> Option<TableDataPlanePublicationGuards> {
|
||||
req.publication_guards().cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn apply_copy_source_bucket_generation_guard<T>(
|
||||
req: &S3Request<T>,
|
||||
pub(crate) fn apply_copy_source_bucket_generation_guard<R: AuthorizationRequest + ?Sized>(
|
||||
req: &R,
|
||||
bucket: &str,
|
||||
opts: &mut ObjectOptions,
|
||||
) -> S3Result<()> {
|
||||
let Some(guard) = req.extensions.get::<CopySourceBucketGenerationGuard>() else {
|
||||
if req.extensions.get::<std::sync::Arc<ServerContextSlot>>().is_some() {
|
||||
return Err(s3_error!(InternalError, "copy source bucket generation guard is missing"));
|
||||
let guard = req.copy_source_bucket_generation();
|
||||
apply_generation_fence(
|
||||
req,
|
||||
guard.map(|guard| (guard.bucket.as_str(), guard.incarnation_id)),
|
||||
bucket,
|
||||
opts,
|
||||
"copy source bucket generation guard",
|
||||
)
|
||||
}
|
||||
|
||||
/// Binds `opts` to the captured incarnation of `bucket` and to the retained table publication
|
||||
/// locks. A server request without a captured guard is refused: only direct use-case callers,
|
||||
/// which carry no server context, run unfenced.
|
||||
fn apply_generation_fence<R: AuthorizationRequest + ?Sized>(
|
||||
req: &R,
|
||||
guard: Option<(&str, uuid::Uuid)>,
|
||||
bucket: &str,
|
||||
opts: &mut ObjectOptions,
|
||||
guard_name: &str,
|
||||
) -> S3Result<()> {
|
||||
match guard {
|
||||
Some((guarded, _)) if guarded != bucket => {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
format!("{guard_name} does not match request bucket"),
|
||||
));
|
||||
}
|
||||
if let Some(publication_guards) = req.extensions.get::<TableDataPlanePublicationGuards>() {
|
||||
publication_guards.add_lock_loss_fences(opts);
|
||||
Some((_, incarnation_id)) => opts.expected_bucket_incarnation_id = Some(incarnation_id),
|
||||
None if req.server_context().is_some() => {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, format!("{guard_name} is missing")));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if guard.bucket != bucket {
|
||||
return Err(s3_error!(
|
||||
InternalError,
|
||||
"copy source bucket generation guard does not match request bucket"
|
||||
));
|
||||
None => {}
|
||||
}
|
||||
opts.expected_bucket_incarnation_id = Some(guard.incarnation_id);
|
||||
if let Some(publication_guards) = req.extensions.get::<TableDataPlanePublicationGuards>() {
|
||||
if let Some(publication_guards) = req.publication_guards() {
|
||||
publication_guards.add_lock_loss_fences(opts);
|
||||
}
|
||||
Ok(())
|
||||
@@ -726,12 +754,12 @@ fn missing_req_info_error() -> S3Error {
|
||||
s3_error!(InternalError, "ReqInfo not found in request extensions")
|
||||
}
|
||||
|
||||
pub(crate) fn req_info_ref<T>(req: &S3Request<T>) -> S3Result<&ReqInfo> {
|
||||
req.extensions.get::<ReqInfo>().ok_or_else(missing_req_info_error)
|
||||
pub(crate) fn req_info_ref<R: AuthorizationRequest + ?Sized>(req: &R) -> S3Result<&ReqInfo> {
|
||||
req.req_info().ok_or_else(missing_req_info_error)
|
||||
}
|
||||
|
||||
pub(crate) fn req_info_mut<T>(req: &mut S3Request<T>) -> S3Result<&mut ReqInfo> {
|
||||
req.extensions.get_mut::<ReqInfo>().ok_or_else(missing_req_info_error)
|
||||
pub(crate) fn req_info_mut<R: AuthorizationRequest + ?Sized>(req: &mut R) -> S3Result<&mut ReqInfo> {
|
||||
req.req_info_mut().ok_or_else(missing_req_info_error)
|
||||
}
|
||||
|
||||
fn ext_req_info_mut(ext: &mut http::Extensions) -> S3Result<&mut ReqInfo> {
|
||||
@@ -740,17 +768,10 @@ fn ext_req_info_mut(ext: &mut http::Extensions) -> S3Result<&mut ReqInfo> {
|
||||
|
||||
/// Extract the canonical `RequestContext` from a request, checking both
|
||||
/// the request extensions directly and the `ReqInfo.request_context` field.
|
||||
pub(crate) fn request_context_from_req<T>(req: &S3Request<T>) -> Option<RequestContext> {
|
||||
request_context_from_extensions(&req.extensions)
|
||||
}
|
||||
|
||||
/// Same lookup against the extensions alone, for callers that have already
|
||||
/// moved a field out of the request and can no longer borrow it whole.
|
||||
pub(crate) fn request_context_from_extensions(extensions: &http::Extensions) -> Option<RequestContext> {
|
||||
extensions
|
||||
.get::<RequestContext>()
|
||||
pub(crate) fn request_context_from_req<R: AuthorizationRequest + ?Sized>(req: &R) -> Option<RequestContext> {
|
||||
req.request_context()
|
||||
.cloned()
|
||||
.or_else(|| extensions.get::<ReqInfo>().and_then(|ri| ri.request_context.clone()))
|
||||
.or_else(|| req.req_info().and_then(|req_info| req_info.request_context.clone()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -990,6 +1011,18 @@ pub(crate) trait AuthorizationRequest {
|
||||
fn server_context(&self) -> Option<&Arc<ServerContextSlot>>;
|
||||
fn req_info(&self) -> Option<&ReqInfo>;
|
||||
fn req_info_mut(&mut self) -> Option<&mut ReqInfo>;
|
||||
fn request_context(&self) -> Option<&RequestContext>;
|
||||
fn bucket_generation(&self) -> Option<&BucketGenerationGuard>;
|
||||
fn install_bucket_generation(&mut self, guard: BucketGenerationGuard);
|
||||
fn pending_delete_bucket_generation(&self) -> Option<&PendingDeleteBucketGenerationGuard>;
|
||||
fn install_pending_delete_bucket_generation(&mut self, guard: PendingDeleteBucketGenerationGuard);
|
||||
fn copy_source_bucket_generation(&self) -> Option<&CopySourceBucketGenerationGuard>;
|
||||
fn odm_read_generation(&self) -> Option<&OdmReadGenerationGuard>;
|
||||
fn install_odm_read_generation(&mut self, guard: OdmReadGenerationGuard);
|
||||
fn bucket_config_mutation(&self) -> Option<&BucketConfigMutationSnapshot>;
|
||||
/// The extensions no typed accessor covers, for test-only payloads.
|
||||
#[cfg(test)]
|
||||
fn test_extensions(&self) -> &http::Extensions;
|
||||
fn internal_object_authorization(&self) -> bool;
|
||||
fn multipart_staging(&self) -> bool;
|
||||
fn staged_multipart_part(&self) -> bool;
|
||||
@@ -1035,6 +1068,37 @@ impl<T> AuthorizationRequest for S3Request<T> {
|
||||
fn req_info_mut(&mut self) -> Option<&mut ReqInfo> {
|
||||
self.extensions.get_mut::<ReqInfo>()
|
||||
}
|
||||
fn request_context(&self) -> Option<&RequestContext> {
|
||||
self.extensions.get::<RequestContext>()
|
||||
}
|
||||
fn bucket_generation(&self) -> Option<&BucketGenerationGuard> {
|
||||
self.extensions.get::<BucketGenerationGuard>()
|
||||
}
|
||||
fn install_bucket_generation(&mut self, guard: BucketGenerationGuard) {
|
||||
self.extensions.insert(guard);
|
||||
}
|
||||
fn pending_delete_bucket_generation(&self) -> Option<&PendingDeleteBucketGenerationGuard> {
|
||||
self.extensions.get::<PendingDeleteBucketGenerationGuard>()
|
||||
}
|
||||
fn install_pending_delete_bucket_generation(&mut self, guard: PendingDeleteBucketGenerationGuard) {
|
||||
self.extensions.insert(guard);
|
||||
}
|
||||
fn copy_source_bucket_generation(&self) -> Option<&CopySourceBucketGenerationGuard> {
|
||||
self.extensions.get::<CopySourceBucketGenerationGuard>()
|
||||
}
|
||||
fn odm_read_generation(&self) -> Option<&OdmReadGenerationGuard> {
|
||||
self.extensions.get::<OdmReadGenerationGuard>()
|
||||
}
|
||||
fn install_odm_read_generation(&mut self, guard: OdmReadGenerationGuard) {
|
||||
self.extensions.insert(guard);
|
||||
}
|
||||
fn bucket_config_mutation(&self) -> Option<&BucketConfigMutationSnapshot> {
|
||||
self.extensions.get::<BucketConfigMutationSnapshot>()
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn test_extensions(&self) -> &http::Extensions {
|
||||
&self.extensions
|
||||
}
|
||||
fn internal_object_authorization(&self) -> bool {
|
||||
self.extensions.get::<InternalObjectAuthorization>().is_some()
|
||||
}
|
||||
@@ -1349,12 +1413,11 @@ fn retain_internal_object_authorization_conditions(
|
||||
/// Lives here rather than at the call site because this module already owns the request
|
||||
/// plumbing every such decision needs: the verified client address and `ReqInfo`. The KMS
|
||||
/// grammar has no bucket/object/tag conditions, so none of the S3 merges apply.
|
||||
pub(crate) fn resource_free_condition_values<T>(
|
||||
req: &S3Request<T>,
|
||||
pub(crate) fn resource_free_condition_values<R: AuthorizationRequest + ?Sized>(
|
||||
req: &R,
|
||||
cred: &rustfs_credentials::Credentials,
|
||||
) -> HashMap<String, Vec<String>> {
|
||||
let remote_addr = req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0));
|
||||
get_condition_values_with_client_info(&req.headers, cred, None, None, remote_addr, req.extensions.get::<ClientInfo>())
|
||||
get_condition_values_with_client_info(req.headers(), cred, None, None, req.remote_addr(), req.client_info())
|
||||
}
|
||||
|
||||
fn request_object_store<R: AuthorizationRequest + ?Sized>(req: &R) -> S3Result<Arc<ECStore>> {
|
||||
@@ -1692,7 +1755,7 @@ pub(crate) fn delete_object_authorize_action(version_id: Option<&str>) -> Action
|
||||
/// Note: `ActionSet::is_match` still maps a `s3:GetObjectVersion` grant onto a
|
||||
/// `s3:GetObject` request. It only broadens a Version grant toward current
|
||||
/// reads, never the reverse.
|
||||
fn versioned_read_action(version_id: Option<&str>) -> Action {
|
||||
pub(crate) fn versioned_read_action(version_id: Option<&str>) -> Action {
|
||||
if version_id.is_some() {
|
||||
Action::S3Action(S3Action::GetObjectVersionAction)
|
||||
} else {
|
||||
@@ -1700,7 +1763,7 @@ fn versioned_read_action(version_id: Option<&str>) -> Action {
|
||||
}
|
||||
}
|
||||
|
||||
fn versioned_tagging_action(action: S3Action, version_id: Option<&str>) -> Action {
|
||||
pub(crate) fn versioned_tagging_action(action: S3Action, version_id: Option<&str>) -> Action {
|
||||
let action = if version_id.is_some() {
|
||||
match action {
|
||||
S3Action::GetObjectTaggingAction => S3Action::GetObjectVersionTaggingAction,
|
||||
@@ -1913,6 +1976,29 @@ pub(crate) async fn authorize_bucket_request<R: AuthorizationRequest + ?Sized>(
|
||||
authorize_request(req, action).await
|
||||
}
|
||||
|
||||
/// Authorizes `action` on one object version for either edge: the whole access hook of an
|
||||
/// operation that names a bucket, a key and an optional version and asks one action, such as
|
||||
/// GetObjectAcl. A caller whose action depends on the version picks it first
|
||||
/// ([`versioned_read_action`], [`versioned_tagging_action`]).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InternalError` when the access phase installed no `ReqInfo`; otherwise what
|
||||
/// [`authorize_request`] refuses.
|
||||
pub(crate) async fn authorize_object_request<R: AuthorizationRequest + ?Sized>(
|
||||
req: &mut R,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
version_id: Option<&str>,
|
||||
action: Action,
|
||||
) -> S3Result<()> {
|
||||
let req_info = req_info_mut(req)?;
|
||||
req_info.bucket = Some(bucket.to_owned());
|
||||
req_info.object = Some(key.to_owned());
|
||||
req_info.version_id = version_id.map(str::to_owned);
|
||||
authorize_request(req, action).await
|
||||
}
|
||||
|
||||
/// Authorize the exact tag set selected by CopyObject's source reader before publication.
|
||||
/// The access hook also calls this with a preliminary snapshot, so tag-dependent
|
||||
/// destination Allow statements can be evaluated before opening either object.
|
||||
@@ -2942,13 +3028,13 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn abort_multipart_upload(&self, req: &mut S3Request<AbortMultipartUploadInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::AbortMultipartUploadAction)).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2957,7 +3043,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn complete_multipart_upload(&self, req: &mut S3Request<CompleteMultipartUploadInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
@@ -2966,7 +3052,7 @@ impl S3Access for FS {
|
||||
let specific_etag = req.input.if_match.as_ref().is_some_and(|etag| !etag.is_any());
|
||||
authorize_write_etag_condition(req, specific_etag).await?;
|
||||
authorize_replication_only_put_headers(req).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2975,7 +3061,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn copy_object(&self, req: &mut S3Request<CopyObjectInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let (src_bucket, src_key, version_id) = match &req.input.copy_source {
|
||||
CopySource::AccessPoint { .. } => return Err(s3_error!(NotImplemented)),
|
||||
CopySource::Outpost { .. } => return Err(s3_error!(NotImplemented)),
|
||||
@@ -3071,14 +3157,14 @@ impl S3Access for FS {
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectRetentionAction)).await?;
|
||||
}
|
||||
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks whether the CreateMultipartUpload request has accesses to the resources.
|
||||
async fn create_multipart_upload(&self, req: &mut S3Request<CreateMultipartUploadInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
@@ -3098,7 +3184,7 @@ impl S3Access for FS {
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectRetentionAction)).await?;
|
||||
}
|
||||
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3250,7 +3336,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn delete_object(&self, req: &mut S3Request<DeleteObjectInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
#[cfg(test)]
|
||||
wait_for_object_generation_test_hook(&bucket).await;
|
||||
// Preserve DeleteObject's established NoSuchBucket response instead of
|
||||
@@ -3312,8 +3398,7 @@ impl S3Access for FS {
|
||||
authorize_request(req, Action::S3Action(S3Action::BypassGovernanceRetentionAction)).await?;
|
||||
}
|
||||
|
||||
req.extensions
|
||||
.insert(PendingDeleteBucketGenerationGuard::from_result(bucket_generation));
|
||||
req.install_pending_delete_bucket_generation(PendingDeleteBucketGenerationGuard::from_result(bucket_generation));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3564,7 +3649,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn get_object(&self, req: &mut S3Request<GetObjectInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let source_generation = capture_odm_read_generation(self, req, &bucket).await;
|
||||
let source_generation = capture_odm_read_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
@@ -3573,7 +3658,7 @@ impl S3Access for FS {
|
||||
// GHSA-3ppv: a versioned read (?versionId=...) must authorize against
|
||||
// s3:GetObjectVersion, not s3:GetObject.
|
||||
authorize_request(req, versioned_read_action(req.input.version_id.as_deref())).await?;
|
||||
req.extensions.insert(source_generation);
|
||||
req.install_odm_read_generation(source_generation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3581,12 +3666,8 @@ impl S3Access for FS {
|
||||
///
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn get_object_acl(&self, req: &mut S3Request<GetObjectAclInput>) -> S3Result<()> {
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::GetObjectAclAction)).await
|
||||
let (bucket, key, version_id) = (req.input.bucket.clone(), req.input.key.clone(), req.input.version_id.clone());
|
||||
authorize_object_request(req, &bucket, &key, version_id.as_deref(), Action::S3Action(S3Action::GetObjectAclAction)).await
|
||||
}
|
||||
|
||||
/// Checks whether the GetObjectAttributes request has accesses to the resources.
|
||||
@@ -3613,12 +3694,15 @@ impl S3Access for FS {
|
||||
///
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn get_object_legal_hold(&self, req: &mut S3Request<GetObjectLegalHoldInput>) -> S3Result<()> {
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::GetObjectLegalHoldAction)).await
|
||||
let (bucket, key, version_id) = (req.input.bucket.clone(), req.input.key.clone(), req.input.version_id.clone());
|
||||
authorize_object_request(
|
||||
req,
|
||||
&bucket,
|
||||
&key,
|
||||
version_id.as_deref(),
|
||||
Action::S3Action(S3Action::GetObjectLegalHoldAction),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Checks whether the GetObjectLockConfiguration request has accesses to the resources.
|
||||
@@ -3635,12 +3719,15 @@ impl S3Access for FS {
|
||||
///
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn get_object_retention(&self, req: &mut S3Request<GetObjectRetentionInput>) -> S3Result<()> {
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::GetObjectRetentionAction)).await
|
||||
let (bucket, key, version_id) = (req.input.bucket.clone(), req.input.key.clone(), req.input.version_id.clone());
|
||||
authorize_object_request(
|
||||
req,
|
||||
&bucket,
|
||||
&key,
|
||||
version_id.as_deref(),
|
||||
Action::S3Action(S3Action::GetObjectRetentionAction),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Checks whether the GetObjectTagging request has accesses to the resources.
|
||||
@@ -3689,7 +3776,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn head_object(&self, req: &mut S3Request<HeadObjectInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let source_generation = capture_odm_read_generation(self, req, &bucket).await;
|
||||
let source_generation = capture_odm_read_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
@@ -3703,7 +3790,7 @@ impl S3Access for FS {
|
||||
req_info_mut(req)?.replication_request_authorized = true;
|
||||
}
|
||||
|
||||
req.extensions.insert(source_generation);
|
||||
req.install_odm_read_generation(source_generation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3760,13 +3847,13 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn list_multipart_uploads(&self, req: &mut S3Request<ListMultipartUploadsInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::ListBucketMultipartUploadsAction)).await?;
|
||||
prepare_table_data_plane_list_access(req, &bucket, S3Action::ListBucketMultipartUploadsAction).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3797,12 +3884,12 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn list_objects_v2(&self, req: &mut S3Request<ListObjectsV2Input>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let source_generation = capture_odm_read_generation(self, req, &bucket).await;
|
||||
let source_generation = capture_odm_read_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
|
||||
authorize_bucket_listing(req, &bucket, S3Action::ListBucketAction).await?;
|
||||
req.extensions.insert(source_generation);
|
||||
req.install_odm_read_generation(source_generation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3811,13 +3898,13 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn list_parts(&self, req: &mut S3Request<ListPartsInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
|
||||
authorize_request(req, list_parts_authorize_action()).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4035,7 +4122,7 @@ impl S3Access for FS {
|
||||
let bucket = req.input.bucket.clone();
|
||||
// Snapshot before authorization, but preserve AccessDenied precedence
|
||||
// by exposing any bucket-state error only after authorization succeeds.
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
#[cfg(test)]
|
||||
let bucket_generation = if PUT_GENERATION_READ_QUORUM_FAILURE.try_with(|_| ()).is_ok() {
|
||||
Err(ApiError::from(StorageError::ErasureReadQuorum).into())
|
||||
@@ -4048,7 +4135,7 @@ impl S3Access for FS {
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectAction)).await?;
|
||||
let specific_etag = req.input.if_match.as_ref().is_some_and(|etag| !etag.is_any());
|
||||
authorize_write_etag_condition(req, specific_etag).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
|
||||
if req.method != http::Method::POST {
|
||||
authorize_replication_only_put_headers(req).await?;
|
||||
@@ -4071,12 +4158,8 @@ impl S3Access for FS {
|
||||
///
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn put_object_acl(&self, req: &mut S3Request<PutObjectAclInput>) -> S3Result<()> {
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectAclAction)).await
|
||||
let (bucket, key, version_id) = (req.input.bucket.clone(), req.input.key.clone(), req.input.version_id.clone());
|
||||
authorize_object_request(req, &bucket, &key, version_id.as_deref(), Action::S3Action(S3Action::PutObjectAclAction)).await
|
||||
}
|
||||
|
||||
/// Checks whether the PutObjectLegalHold request has accesses to the resources.
|
||||
@@ -4091,9 +4174,9 @@ impl S3Access for FS {
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectLegalHoldAction)).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4115,7 +4198,7 @@ impl S3Access for FS {
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
set_requested_object_lock_retention(req);
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectRetentionAction)).await?;
|
||||
|
||||
@@ -4124,7 +4207,7 @@ impl S3Access for FS {
|
||||
authorize_request(req, Action::S3Action(S3Action::BypassGovernanceRetentionAction)).await?;
|
||||
}
|
||||
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4160,7 +4243,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn restore_object(&self, req: &mut S3Request<RestoreObjectInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
#[cfg(test)]
|
||||
wait_for_object_generation_test_hook(&bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
@@ -4169,7 +4252,7 @@ impl S3Access for FS {
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
authorize_request(req, Action::S3Action(S3Action::RestoreObjectAction)).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4189,13 +4272,13 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn upload_part(&self, req: &mut S3Request<UploadPartInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
|
||||
authorize_staged_multipart_part(req).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4204,7 +4287,7 @@ impl S3Access for FS {
|
||||
/// This method returns `Ok(())` by default.
|
||||
async fn upload_part_copy(&self, req: &mut S3Request<UploadPartCopyInput>) -> S3Result<()> {
|
||||
let bucket = req.input.bucket.clone();
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let bucket_generation = load_bucket_generation(self.server_ctx(), req, &bucket).await;
|
||||
let (src_bucket, src_key, version_id) = match &req.input.copy_source {
|
||||
CopySource::AccessPoint { .. } => return Err(s3_error!(NotImplemented)),
|
||||
CopySource::Outpost { .. } => return Err(s3_error!(NotImplemented)),
|
||||
@@ -4237,7 +4320,7 @@ impl S3Access for FS {
|
||||
req_info.version_id = None;
|
||||
|
||||
authorize_staged_multipart_part(req).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
req.install_bucket_generation(bucket_generation?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Owned-versus-legacy parity of the state an access hook hands to a use case: every reader and
|
||||
//! installer must answer the same through an s3s request and through the `RequestEnvelope` built
|
||||
//! from it, with the state present and absent, and each answer is pinned to its expected value so
|
||||
//! the parity cannot hold vacuously.
|
||||
//!
|
||||
//! Not covered here: policy decisions (`app/owned_authorization_test.rs`).
|
||||
//! Upstream: the readers in `storage/access.rs`; downstream: none.
|
||||
|
||||
use super::{
|
||||
Action, AuthorizationRequest, BucketConfigMutationSnapshot, BucketGenerationGuard, CopySourceBucketGenerationGuard, HashMap,
|
||||
HeaderMap, ObjectOptions, OdmReadGenerationGuard, PendingDeleteBucketGenerationGuard, RemoteAddr, ReqInfo, RequestContext,
|
||||
S3Action, S3ErrorCode, S3Request, S3Result, TableDataPlanePublicationGuards, apply_bucket_generation_guard,
|
||||
apply_copy_source_bucket_generation_guard, authorize_object_request, bucket_config_mutation_incarnation,
|
||||
capture_odm_read_generation, odm_read_generation, replication_request_authorized, request_context_from_req,
|
||||
resource_free_condition_values, retained_table_data_plane_publication_guards,
|
||||
};
|
||||
use crate::app::RequestEnvelope;
|
||||
use crate::runtime_sources::ServerContextSlot;
|
||||
use rustfs_trusted_proxies::ClientInfo;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
const BUCKET: &str = "guarded-bucket";
|
||||
|
||||
fn incarnation() -> uuid::Uuid {
|
||||
uuid::Uuid::from_u128(0x7107)
|
||||
}
|
||||
|
||||
fn guard(bucket: &str) -> BucketGenerationGuard {
|
||||
BucketGenerationGuard {
|
||||
bucket: bucket.to_owned(),
|
||||
incarnation_id: incarnation(),
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy(fill: impl FnOnce(&mut http::Extensions)) -> S3Request<()> {
|
||||
let mut extensions = http::Extensions::new();
|
||||
fill(&mut extensions);
|
||||
S3Request {
|
||||
input: (),
|
||||
method: http::Method::GET,
|
||||
uri: "/guarded-bucket/key".parse().expect("static uri"),
|
||||
headers: HeaderMap::new(),
|
||||
extensions,
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn server_request(extensions: &mut http::Extensions) {
|
||||
extensions.insert(ServerContextSlot::new());
|
||||
}
|
||||
|
||||
/// The incarnation a reader bound, or the refusal's code and message.
|
||||
type Answer = Result<Option<uuid::Uuid>, (S3ErrorCode, String)>;
|
||||
|
||||
fn answer<T>(result: S3Result<T>, bound: impl FnOnce(T) -> Option<uuid::Uuid>) -> Answer {
|
||||
result
|
||||
.map(bound)
|
||||
.map_err(|err| (err.code().clone(), err.message().unwrap_or_default().to_owned()))
|
||||
}
|
||||
|
||||
fn refused(code: S3ErrorCode, message: &str) -> Answer {
|
||||
Err((code, message.to_owned()))
|
||||
}
|
||||
|
||||
fn bucket_fence<R: AuthorizationRequest + ?Sized>(req: &R) -> Answer {
|
||||
let mut opts = ObjectOptions::default();
|
||||
let result = apply_bucket_generation_guard(req, BUCKET, &mut opts);
|
||||
answer(result, |()| opts.expected_bucket_incarnation_id)
|
||||
}
|
||||
|
||||
fn copy_source_fence<R: AuthorizationRequest + ?Sized>(req: &R) -> Answer {
|
||||
let mut opts = ObjectOptions::default();
|
||||
let result = apply_copy_source_bucket_generation_guard(req, BUCKET, &mut opts);
|
||||
answer(result, |()| opts.expected_bucket_incarnation_id)
|
||||
}
|
||||
|
||||
fn odm<R: AuthorizationRequest + ?Sized>(req: &R) -> Answer {
|
||||
answer(odm_read_generation(req, BUCKET), |id| id)
|
||||
}
|
||||
|
||||
fn config_mutation<R: AuthorizationRequest + ?Sized>(req: &R) -> Answer {
|
||||
answer(bucket_config_mutation_incarnation(req, BUCKET), |id| id)
|
||||
}
|
||||
|
||||
/// Asserts that `read` gives `expected` through both request forms of every case.
|
||||
fn assert_cases(
|
||||
read: fn(&S3Request<()>) -> Answer,
|
||||
owned: fn(&RequestEnvelope) -> Answer,
|
||||
cases: Vec<(&str, S3Request<()>, Answer)>,
|
||||
) {
|
||||
for (name, req, expected) in cases {
|
||||
let envelope = RequestEnvelope::from_s3s(&req);
|
||||
assert_eq!(read(&req), expected, "legacy form: {name}");
|
||||
assert_eq!(owned(&envelope), expected, "owned form: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_generation_guard_reads_the_same_through_both_forms() {
|
||||
let missing = "bucket generation guard is missing";
|
||||
let mismatch = "bucket generation guard does not match request bucket";
|
||||
assert_cases(
|
||||
bucket_fence,
|
||||
bucket_fence,
|
||||
vec![
|
||||
(
|
||||
"captured guard binds",
|
||||
legacy(|ext| {
|
||||
ext.insert(guard(BUCKET));
|
||||
}),
|
||||
Ok(Some(incarnation())),
|
||||
),
|
||||
(
|
||||
"pending-delete guard wins over the captured guard",
|
||||
legacy(|ext| {
|
||||
ext.insert(guard("other-bucket"));
|
||||
ext.insert(PendingDeleteBucketGenerationGuard::Ready(guard(BUCKET)));
|
||||
}),
|
||||
Ok(Some(incarnation())),
|
||||
),
|
||||
(
|
||||
"guard of another bucket",
|
||||
legacy(|ext| {
|
||||
ext.insert(guard("other-bucket"));
|
||||
}),
|
||||
refused(S3ErrorCode::InternalError, mismatch),
|
||||
),
|
||||
(
|
||||
"failed pending-delete capture",
|
||||
legacy(|ext| {
|
||||
ext.insert(guard(BUCKET));
|
||||
ext.insert(PendingDeleteBucketGenerationGuard::Failed {
|
||||
code: S3ErrorCode::NoSuchBucket,
|
||||
message: "bucket is gone".to_owned(),
|
||||
});
|
||||
}),
|
||||
refused(S3ErrorCode::NoSuchBucket, "bucket is gone"),
|
||||
),
|
||||
(
|
||||
"server request without a guard",
|
||||
legacy(server_request),
|
||||
refused(S3ErrorCode::InternalError, missing),
|
||||
),
|
||||
("direct use-case call without a guard", legacy(|_| {}), Ok(None)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_source_generation_guard_reads_the_same_through_both_forms() {
|
||||
let copy_guard = |bucket: &str| CopySourceBucketGenerationGuard {
|
||||
bucket: bucket.to_owned(),
|
||||
incarnation_id: incarnation(),
|
||||
};
|
||||
assert_cases(
|
||||
copy_source_fence,
|
||||
copy_source_fence,
|
||||
vec![
|
||||
(
|
||||
"captured guard binds",
|
||||
legacy(|ext| {
|
||||
ext.insert(copy_guard(BUCKET));
|
||||
}),
|
||||
Ok(Some(incarnation())),
|
||||
),
|
||||
(
|
||||
"guard of another bucket",
|
||||
legacy(|ext| {
|
||||
ext.insert(copy_guard("other-bucket"));
|
||||
}),
|
||||
refused(
|
||||
S3ErrorCode::InternalError,
|
||||
"copy source bucket generation guard does not match request bucket",
|
||||
),
|
||||
),
|
||||
(
|
||||
"the destination guard does not stand in for the source",
|
||||
legacy(|ext| {
|
||||
server_request(ext);
|
||||
ext.insert(guard(BUCKET));
|
||||
}),
|
||||
refused(S3ErrorCode::InternalError, "copy source bucket generation guard is missing"),
|
||||
),
|
||||
("direct use-case call without a guard", legacy(|_| {}), Ok(None)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odm_read_generation_reads_the_same_through_both_forms() {
|
||||
assert_cases(
|
||||
odm,
|
||||
odm,
|
||||
vec![
|
||||
(
|
||||
"captured source generation",
|
||||
legacy(|ext| {
|
||||
ext.insert(OdmReadGenerationGuard::Ready(guard(BUCKET)));
|
||||
}),
|
||||
Ok(Some(incarnation())),
|
||||
),
|
||||
(
|
||||
"source generation of another bucket",
|
||||
legacy(|ext| {
|
||||
ext.insert(OdmReadGenerationGuard::Ready(guard("other-bucket")));
|
||||
}),
|
||||
refused(S3ErrorCode::InternalError, "source generation guard does not match request bucket"),
|
||||
),
|
||||
(
|
||||
"failed capture surfaces its error",
|
||||
legacy(|ext| {
|
||||
ext.insert(OdmReadGenerationGuard::Failed {
|
||||
code: S3ErrorCode::SlowDown,
|
||||
message: "capture failed".to_owned(),
|
||||
});
|
||||
}),
|
||||
refused(S3ErrorCode::SlowDown, "capture failed"),
|
||||
),
|
||||
(
|
||||
"no source configured",
|
||||
legacy(|ext| {
|
||||
ext.insert(OdmReadGenerationGuard::Unavailable);
|
||||
}),
|
||||
Ok(None),
|
||||
),
|
||||
("nothing captured", legacy(|_| {}), Ok(None)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_config_mutation_incarnation_reads_the_same_through_both_forms() {
|
||||
let snapshot = |bucket: &str| BucketConfigMutationSnapshot {
|
||||
bucket: bucket.to_owned(),
|
||||
incarnation_id: incarnation(),
|
||||
};
|
||||
assert_cases(
|
||||
config_mutation,
|
||||
config_mutation,
|
||||
vec![
|
||||
(
|
||||
"captured snapshot",
|
||||
legacy(|ext| {
|
||||
ext.insert(snapshot(BUCKET));
|
||||
}),
|
||||
Ok(Some(incarnation())),
|
||||
),
|
||||
(
|
||||
"snapshot of another bucket",
|
||||
legacy(|ext| {
|
||||
ext.insert(snapshot("other-bucket"));
|
||||
}),
|
||||
refused(
|
||||
S3ErrorCode::InternalError,
|
||||
"bucket config mutation snapshot does not match request bucket",
|
||||
),
|
||||
),
|
||||
(
|
||||
"server request without a snapshot",
|
||||
legacy(server_request),
|
||||
refused(S3ErrorCode::InternalError, "bucket config mutation snapshot is missing"),
|
||||
),
|
||||
("direct use-case call without a snapshot", legacy(|_| {}), Ok(None)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_state_reads_back_through_the_envelope() {
|
||||
let mut envelope = RequestEnvelope::from_s3s(&legacy(server_request));
|
||||
assert_eq!(
|
||||
bucket_fence(&envelope),
|
||||
refused(S3ErrorCode::InternalError, "bucket generation guard is missing")
|
||||
);
|
||||
envelope.install_bucket_generation(guard(BUCKET));
|
||||
assert_eq!(bucket_fence(&envelope), Ok(Some(incarnation())));
|
||||
envelope.install_pending_delete_bucket_generation(PendingDeleteBucketGenerationGuard::Failed {
|
||||
code: S3ErrorCode::NoSuchBucket,
|
||||
message: "bucket is gone".to_owned(),
|
||||
});
|
||||
assert_eq!(bucket_fence(&envelope), refused(S3ErrorCode::NoSuchBucket, "bucket is gone"));
|
||||
|
||||
assert_eq!(odm(&envelope), Ok(None));
|
||||
envelope.install_odm_read_generation(OdmReadGenerationGuard::Ready(guard("other-bucket")));
|
||||
assert_eq!(
|
||||
odm(&envelope),
|
||||
refused(S3ErrorCode::InternalError, "source generation guard does not match request bucket")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn odm_capture_without_a_configured_source_is_unavailable_in_both_forms() {
|
||||
let slot = ServerContextSlot::new();
|
||||
let req = legacy(|ext| {
|
||||
ext.insert(guard(BUCKET));
|
||||
});
|
||||
let envelope = RequestEnvelope::from_s3s(&req);
|
||||
for captured in [
|
||||
capture_odm_read_generation(&slot, &req, BUCKET).await,
|
||||
capture_odm_read_generation(&slot, &envelope, BUCKET).await,
|
||||
] {
|
||||
assert!(matches!(captured, OdmReadGenerationGuard::Unavailable), "{captured:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replication_authorization_flag_reads_the_same_through_both_forms() {
|
||||
let with_flag = |flag: bool| {
|
||||
legacy(move |ext| {
|
||||
ext.insert(ReqInfo {
|
||||
replication_request_authorized: flag,
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
};
|
||||
for (name, req, expected) in [
|
||||
("authorized replication request", with_flag(true), true),
|
||||
("ordinary request", with_flag(false), false),
|
||||
("request without ReqInfo", legacy(|_| {}), false),
|
||||
] {
|
||||
let envelope = RequestEnvelope::from_s3s(&req);
|
||||
assert_eq!(replication_request_authorized(&req), expected, "legacy form: {name}");
|
||||
assert_eq!(replication_request_authorized(&envelope), expected, "owned form: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_guards_and_request_context_read_the_same_through_both_forms() {
|
||||
let context = |id: &str| RequestContext {
|
||||
request_id: id.to_owned(),
|
||||
x_amz_request_id: id.to_owned(),
|
||||
trace_id: None,
|
||||
span_id: None,
|
||||
start_time: std::time::Instant::now(),
|
||||
};
|
||||
let nested = |id: &str| ReqInfo {
|
||||
request_context: Some(context(id)),
|
||||
..Default::default()
|
||||
};
|
||||
for (name, req, expected) in [
|
||||
(
|
||||
"direct context",
|
||||
legacy(|ext| {
|
||||
ext.insert(context("direct"));
|
||||
}),
|
||||
Some("direct"),
|
||||
),
|
||||
(
|
||||
"context inside ReqInfo",
|
||||
legacy(|ext| {
|
||||
ext.insert(nested("nested"));
|
||||
}),
|
||||
Some("nested"),
|
||||
),
|
||||
(
|
||||
"the direct context wins",
|
||||
legacy(|ext| {
|
||||
ext.insert(context("direct"));
|
||||
ext.insert(nested("nested"));
|
||||
}),
|
||||
Some("direct"),
|
||||
),
|
||||
("no context", legacy(|_| {}), None),
|
||||
] {
|
||||
let envelope = RequestEnvelope::from_s3s(&req);
|
||||
let id = |found: Option<RequestContext>| found.map(|context| context.request_id);
|
||||
assert_eq!(id(request_context_from_req(&req)).as_deref(), expected, "legacy form: {name}");
|
||||
assert_eq!(id(request_context_from_req(&envelope)).as_deref(), expected, "owned form: {name}");
|
||||
}
|
||||
|
||||
let with_guards = legacy(|ext| {
|
||||
ext.insert(TableDataPlanePublicationGuards::default());
|
||||
});
|
||||
assert!(retained_table_data_plane_publication_guards(&with_guards).is_some());
|
||||
assert!(retained_table_data_plane_publication_guards(&RequestEnvelope::from_s3s(&with_guards)).is_some());
|
||||
let without = legacy(|_| {});
|
||||
assert!(retained_table_data_plane_publication_guards(&without).is_none());
|
||||
assert!(retained_table_data_plane_publication_guards(&RequestEnvelope::from_s3s(&without)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_free_conditions_read_the_same_client_through_both_forms() {
|
||||
let peer: SocketAddr = "192.0.2.2:9000".parse().expect("peer address");
|
||||
let client: SocketAddr = "198.51.100.7:443".parse().expect("client address");
|
||||
let cred = rustfs_credentials::Credentials {
|
||||
access_key: "condition-user".to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
let source_ip = |conditions: &HashMap<String, Vec<String>>| conditions.get("SourceIp").cloned();
|
||||
for (name, req, expected) in [
|
||||
(
|
||||
"forwarded client wins over the peer",
|
||||
legacy(|ext| {
|
||||
ext.insert(Some(RemoteAddr(peer)));
|
||||
ext.insert(ClientInfo::direct(client));
|
||||
}),
|
||||
Some(vec!["198.51.100.7".to_owned()]),
|
||||
),
|
||||
(
|
||||
"peer only",
|
||||
legacy(|ext| {
|
||||
ext.insert(Some(RemoteAddr(peer)));
|
||||
}),
|
||||
Some(vec!["192.0.2.2".to_owned()]),
|
||||
),
|
||||
] {
|
||||
let envelope = RequestEnvelope::from_s3s(&req);
|
||||
let legacy_conditions = resource_free_condition_values(&req, &cred);
|
||||
assert_eq!(legacy_conditions, resource_free_condition_values(&envelope, &cred), "{name}");
|
||||
assert_eq!(source_ip(&legacy_conditions), expected, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn object_request_without_req_info_is_refused_in_both_forms() {
|
||||
let mut req = legacy(|_| {});
|
||||
let mut envelope = RequestEnvelope::from_s3s(&req);
|
||||
let action = Action::S3Action(S3Action::GetObjectAclAction);
|
||||
for result in [
|
||||
authorize_object_request(&mut req, BUCKET, "key", Some("v1"), action).await,
|
||||
authorize_object_request(&mut envelope, BUCKET, "key", Some("v1"), action).await,
|
||||
] {
|
||||
let err = result.expect_err("a request without ReqInfo is refused");
|
||||
assert_eq!(err.code(), &S3ErrorCode::InternalError);
|
||||
assert_eq!(err.message(), Some("ReqInfo not found in request extensions"));
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ use super::storage_api::ecstore_object::{
|
||||
ReadEncryptionMode, ReadEncryptionRequest,
|
||||
};
|
||||
use crate::runtime_sources::current_kms_runtime_service_manager;
|
||||
use crate::storage::access::{ReqInfo, request_context_from_req, resource_free_condition_values};
|
||||
use crate::storage::access::{AuthorizationRequest, request_context_from_req, resource_free_condition_values};
|
||||
use crate::storage::storage_api::runtime_sources_consumer::runtime_sources;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use aes_gcm::aead::Payload;
|
||||
@@ -100,7 +100,6 @@ use rustfs_policy::policy::Args;
|
||||
use rustfs_policy::policy::action::{Action, KmsAction};
|
||||
use rustfs_utils::{get_env_bool, get_env_opt_str};
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::S3Request;
|
||||
use s3s::dto::ServerSideEncryption;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -898,8 +897,8 @@ impl SseKmsPrincipal {
|
||||
/// Anonymous requests use an empty account, matching the bucket-policy authorization
|
||||
/// path. This keeps every request that crossed the S3 boundary distinct from trusted
|
||||
/// internal callers, which are represented by `None`.
|
||||
pub(crate) fn from_request<T>(req: &S3Request<T>) -> Option<Self> {
|
||||
let req_info = req.extensions.get::<ReqInfo>()?;
|
||||
pub(crate) fn from_request<R: AuthorizationRequest + ?Sized>(req: &R) -> Option<Self> {
|
||||
let req_info = req.req_info()?;
|
||||
let cred = req_info.cred.as_ref();
|
||||
|
||||
Some(Self {
|
||||
@@ -910,7 +909,7 @@ impl SseKmsPrincipal {
|
||||
conditions: cred.map(|cred| resource_free_condition_values(req, cred)).unwrap_or_default(),
|
||||
request_audit: request_context_from_req(req).and_then(|context| kms_request_audit(&context.request_id)),
|
||||
#[cfg(test)]
|
||||
test_hooks: req.extensions.get::<TestAuthorizationHooks>().cloned(),
|
||||
test_hooks: req.test_extensions().get::<TestAuthorizationHooks>().cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ pub(crate) mod owned_parity_consumer {
|
||||
pub(crate) use super::super::access::{
|
||||
ObjectTagConditions, PostObjectPolicyHeaders, PostObjectRequestMarker, ReqInfo, RequestedCopySource,
|
||||
RequestedObjectLegalHold, RequestedObjectLockRetainUntil, RequestedObjectLockRetention, RequestedObjectTags,
|
||||
TableDataPlanePublicationGuards, authorize_request, parity_test_support,
|
||||
TableDataPlanePublicationGuards, authorize_object_request, authorize_request, parity_test_support,
|
||||
};
|
||||
pub(crate) use super::super::denial_audit::{DenialAuditor, DenialRateLimiter, SUPPRESSED_DENIALS_TAG};
|
||||
pub(crate) use super::super::ecfs::FS;
|
||||
|
||||
Reference in New Issue
Block a user