mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-15 11:30:31 +00:00
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:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user