feat(gateway): serve GetBucketLocation on both stacks

Route the operation through the owned use case and shared authorization.
The tests cover two bucket names and a missing bucket on both stacks.

Validation on 20ea63e27: both clippy configurations, targeted nextest
(113 passed), and test wiring. Bypassing location authorization made the
gateway refusal matrix fail for anonymous and no-policy callers. The main
legacy comparison passed. Non-default regions were not verified.
This commit is contained in:
overtrue
2026-11-13 11:26:44 +08:00
parent e83b6815ed
commit e97dc74d93
5 changed files with 86 additions and 24 deletions
+21 -19
View File
@@ -16,9 +16,8 @@
use super::*;
use crate::app::storage_api::bucket_usecase::access::requested_bucket_force_delete;
use crate::app::storage_api::s3::dto::{BucketLocationConstraint, GetBucketLocationInput, GetBucketLocationOutput};
use crate::app::storage_api::s3::S3Error;
use crate::app::storage_api::s3::region::Region;
use crate::app::storage_api::s3::{S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
/// Whether `CreateBucket` may record `location`: the server's own region, or
/// one of the further regions listed after it in `RUSTFS_REGION`. `location`
@@ -323,27 +322,32 @@ impl DefaultBucketUsecase {
Ok(gateway_dto::HeadBucketOutput::default())
}
#[instrument(level = "debug", skip(self, req))]
/// Answers the region `input.bucket` is in, for either edge: none for the default region,
/// as AWS answers for `us-east-1`. The access phase has authorized it.
///
/// # Errors
///
/// `InternalError` when the store is not initialized; otherwise the storage error of the
/// lookup, `NoSuchBucket` among them.
#[instrument(level = "debug", skip(self, input))]
pub async fn execute_get_bucket_location(
&self,
req: S3Request<GetBucketLocationInput>,
) -> S3Result<S3Response<GetBucketLocationOutput>> {
let input = req.input;
input: gateway_dto::GetBucketLocationInput,
) -> rustfs_s3_types::S3Result<gateway_dto::GetBucketLocationOutput> {
let Some(store) = self.object_store() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
return Err(rustfs_s3_types::s3_error!(InternalError, "Not init"));
};
store
.get_bucket_info(&input.bucket, &BucketOptions::default())
.get_bucket_info(input.bucket.as_str(), &BucketOptions::default())
.await
.map_err(ApiError::from)?;
let location_constraint = self
.global_region()
.filter(|region| region.as_str() != RUSTFS_REGION)
.map(|region| BucketLocationConstraint::from(region.to_string()));
Ok(S3Response::new(GetBucketLocationOutput { location_constraint }))
.map(|region| gateway_dto::LocationConstraint::custom(region.to_string()));
Ok(gateway_dto::GetBucketLocationOutput { location_constraint })
}
}
@@ -540,15 +544,13 @@ mod tests {
#[tokio::test]
async fn execute_get_bucket_location_returns_internal_error_when_store_uninitialized() {
let input = GetBucketLocationInput::builder()
.bucket("test-bucket".to_string())
.build()
.unwrap();
let req = build_request(input, Method::GET);
let input = gateway_dto::GetBucketLocationInput {
bucket: rustfs_gateway_types::BucketName::new("test-bucket").expect("valid bucket name"),
..Default::default()
};
let usecase = DefaultBucketUsecase::without_context();
let err = usecase.execute_get_bucket_location(req).await.unwrap_err();
assert_eq!(err.code(), &S3ErrorCode::InternalError);
let err = usecase.execute_get_bucket_location(input).await.unwrap_err();
assert_eq!(err.code(), &rustfs_s3_types::S3ErrorCode::InternalError);
}
}
@@ -32,6 +32,7 @@ pub(super) const ROWS: &[OpRow] = &[
row::<dto::HeadBucket>(),
row::<dto::CreateBucket>(),
row::<dto::DeleteBucket>(),
row::<dto::GetBucketLocation>(),
];
impl Served for dto::ListBuckets {
@@ -98,3 +99,19 @@ impl Served for dto::DeleteBucket {
Ok(Resp::new(output))
}
}
impl Served for dto::GetBucketLocation {
const METRIC: S3Operation = S3Operation::GetBucketLocation;
async fn authorize(envelope: &mut RequestEnvelope, request: &Req<Self>) -> S3Result<()> {
let action = Action::S3Action(S3Action::GetBucketLocationAction);
Ok(authorize_bucket_request(envelope, request.input().bucket.as_str(), action).await?)
}
async fn execute(context: Option<Arc<AppContext>>, _envelope: RequestEnvelope, request: Req<Self>) -> S3Result<Resp<Self>> {
let output = DefaultBucketUsecase::with_context(context)
.execute_get_bucket_location(request.into_input())
.await?;
Ok(Resp::new(output))
}
}
+2 -4
View File
@@ -3393,10 +3393,8 @@ impl S3Access for FS {
///
/// This method returns `Ok(())` by default.
async fn get_bucket_location(&self, req: &mut S3Request<GetBucketLocationInput>) -> S3Result<()> {
let req_info = ext_req_info_mut(&mut req.extensions)?;
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::GetBucketLocationAction)).await
let bucket = req.input.bucket.clone();
authorize_bucket_request(req, &bucket, Action::S3Action(S3Action::GetBucketLocationAction)).await
}
/// Checks whether the GetBucketLogging request has accesses to the resources.
@@ -34,6 +34,7 @@ use crate::storage::{
};
use crate::storage::{persisted_from_s3s, persisted_to_s3s};
use rustfs_gateway_types::compat::s3s_5761ddfe::generated::ops::{create_bucket, delete_bucket, head_bucket};
use rustfs_gateway_types::compat::s3s_5761ddfe::get_bucket_location;
use rustfs_io_metrics::record_s3_op;
use rustfs_s3_ops::S3Operation;
use tracing::instrument;
@@ -164,7 +165,13 @@ impl FS {
) -> S3Result<S3Response<GetBucketLocationOutput>> {
record_s3_op(S3Operation::GetBucketLocation);
let usecase = s3_api::bucket_usecase_for(self);
usecase.execute_get_bucket_location(req).await
through_gateway_seam(
req,
get_bucket_location::input_from_s3s,
|output| Ok(get_bucket_location::output_to_s3s(output)),
|_, input| usecase.execute_get_bucket_location(input),
)
.await
}
pub(in crate::storage::ecfs) async fn get_bucket_request_payment_edge(
@@ -46,6 +46,12 @@ pub(crate) const MATRIX: &[MatrixRow] = &[
has_error_body: true,
call: delete_bucket,
},
MatrixRow {
operation: "GetBucketLocation",
metric: S3Operation::GetBucketLocation,
has_error_body: true,
call: get_bucket_location,
},
];
fn list_buckets<'a>(client: &'a Client, _bucket: &'a str) -> BoxFuture<'a, Outcome> {
@@ -56,6 +62,10 @@ fn head_bucket<'a>(client: &'a Client, bucket: &'a str) -> BoxFuture<'a, Outcome
Box::pin(async move { outcome(client.head_bucket().bucket(bucket).send().await) })
}
fn get_bucket_location<'a>(client: &'a Client, bucket: &'a str) -> BoxFuture<'a, Outcome> {
Box::pin(async move { outcome(client.get_bucket_location().bucket(bucket).send().await) })
}
/// Re-creating the existing matrix bucket: the default region answers it `200`.
fn create_bucket<'a>(client: &'a Client, bucket: &'a str) -> BoxFuture<'a, Outcome> {
Box::pin(async move { outcome(client.create_bucket().bucket(bucket).send().await) })
@@ -227,6 +237,34 @@ fn the_legacy_stack_deletes_buckets_as_before() {
with_stack(LEGACY, delete_bucket_answers_as_before);
}
/// A bucket in the default region has an empty location constraint, under an ordinary name and
/// under one the AWS grammar reserves; a missing bucket is `NoSuchBucket`.
async fn get_bucket_location_answers_as_before(server: RustFSServer) {
let root = s3_client(&server.endpoint(), server.access_key(), server.secret_key());
for bucket in ["located", "located-s3alias"] {
super::create_bucket_in_the_store(bucket).await;
let located = root.get_bucket_location().bucket(bucket).send().await.expect(bucket);
// The default region is written as an empty `LocationConstraint` element, which the SDK
// reads as an empty constraint.
assert_eq!(located.location_constraint().map(|constraint| constraint.as_str()), Some(""), "{bucket}");
}
let missing = root.get_bucket_location().bucket("not-located").send().await;
let refusal = outcome(missing).expect_err("a missing bucket is refused");
assert_eq!((refusal.code.as_deref(), refusal.status), (Some("NoSuchBucket"), Some(404)));
server.shutdown().await;
}
#[test]
fn the_gateway_stack_locates_buckets_as_before() {
with_stack(GATEWAY, get_bucket_location_answers_as_before);
}
#[test]
fn the_legacy_stack_locates_buckets_as_before() {
with_stack(LEGACY, get_bucket_location_answers_as_before);
}
async fn force_delete_header_order(server: RustFSServer) {
let endpoint = server.endpoint();
let root = s3_client(&endpoint, server.access_key(), server.secret_key());