refactor s3_router and api_server to make unused Endpoint parameters more obvious
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

This commit is contained in:
trinity-1686a 2022-01-11 19:14:14 +01:00 committed by Trinity Pointard
parent 7c049f1c94
commit 178e35f868
2 changed files with 145 additions and 255 deletions

View file

@ -107,25 +107,25 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
.as_ref() .as_ref()
.and_then(|root_domain| host_to_bucket(&host, root_domain)); .and_then(|root_domain| host_to_bucket(&host, root_domain));
let endpoint = Endpoint::from_request(&req, bucket.map(ToOwned::to_owned))?; let (endpoint, bucket) = Endpoint::from_request(&req, bucket.map(ToOwned::to_owned))?;
debug!("Endpoint: {:?}", endpoint); debug!("Endpoint: {:?}", endpoint);
// Special code path for CreateBucket API endpoint let bucket_name = match bucket {
if let Endpoint::CreateBucket { bucket } = endpoint {
return handle_create_bucket(&garage, req, content_sha256, api_key, bucket).await;
}
let bucket_name = match endpoint.get_bucket() {
None => return handle_request_without_bucket(garage, req, api_key, endpoint).await, None => return handle_request_without_bucket(garage, req, api_key, endpoint).await,
Some(bucket) => bucket.to_string(), Some(bucket) => bucket.to_string(),
}; };
// Special code path for CreateBucket API endpoint
if let Endpoint::CreateBucket {} = endpoint {
return handle_create_bucket(&garage, req, content_sha256, api_key, bucket_name).await;
}
let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?; let bucket_id = resolve_bucket(&garage, &bucket_name, &api_key).await?;
let allowed = match endpoint.authorization_type() { let allowed = match endpoint.authorization_type() {
Authorization::Read(_) => api_key.allow_read(&bucket_id), Authorization::Read => api_key.allow_read(&bucket_id),
Authorization::Write(_) => api_key.allow_write(&bucket_id), Authorization::Write => api_key.allow_write(&bucket_id),
Authorization::Owner(_) => api_key.allow_owner(&bucket_id), Authorization::Owner => api_key.allow_owner(&bucket_id),
_ => unreachable!(), _ => unreachable!(),
}; };
@ -142,7 +142,6 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
key, key,
part_number, part_number,
upload_id, upload_id,
..
} => { } => {
handle_put_part( handle_put_part(
garage, garage,
@ -155,14 +154,11 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
) )
.await .await
} }
Endpoint::CopyObject { key, .. } => { Endpoint::CopyObject { key } => handle_copy(garage, &api_key, &req, bucket_id, &key).await,
handle_copy(garage, &api_key, &req, bucket_id, &key).await
}
Endpoint::UploadPartCopy { Endpoint::UploadPartCopy {
key, key,
part_number, part_number,
upload_id, upload_id,
..
} => { } => {
handle_upload_part_copy( handle_upload_part_copy(
garage, garage,
@ -175,25 +171,21 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
) )
.await .await
} }
Endpoint::PutObject { key, .. } => { Endpoint::PutObject { key } => {
handle_put(garage, req, bucket_id, &key, &api_key, content_sha256).await handle_put(garage, req, bucket_id, &key, &api_key, content_sha256).await
} }
Endpoint::AbortMultipartUpload { key, upload_id, .. } => { Endpoint::AbortMultipartUpload { key, upload_id } => {
handle_abort_multipart_upload(garage, bucket_id, &key, &upload_id).await handle_abort_multipart_upload(garage, bucket_id, &key, &upload_id).await
} }
Endpoint::DeleteObject { key, .. } => handle_delete(garage, bucket_id, &key).await, Endpoint::DeleteObject { key, .. } => handle_delete(garage, bucket_id, &key).await,
Endpoint::CreateMultipartUpload { bucket, key } => { Endpoint::CreateMultipartUpload { key } => {
handle_create_multipart_upload(garage, &req, &bucket, bucket_id, &key).await handle_create_multipart_upload(garage, &req, &bucket_name, bucket_id, &key).await
} }
Endpoint::CompleteMultipartUpload { Endpoint::CompleteMultipartUpload { key, upload_id } => {
bucket,
key,
upload_id,
} => {
handle_complete_multipart_upload( handle_complete_multipart_upload(
garage, garage,
req, req,
&bucket, &bucket_name,
bucket_id, bucket_id,
&key, &key,
&upload_id, &upload_id,
@ -201,19 +193,18 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
) )
.await .await
} }
Endpoint::CreateBucket { .. } => unreachable!(), Endpoint::CreateBucket {} => unreachable!(),
Endpoint::HeadBucket { .. } => { Endpoint::HeadBucket {} => {
let empty_body: Body = Body::from(vec![]); let empty_body: Body = Body::from(vec![]);
let response = Response::builder().body(empty_body).unwrap(); let response = Response::builder().body(empty_body).unwrap();
Ok(response) Ok(response)
} }
Endpoint::DeleteBucket { .. } => { Endpoint::DeleteBucket {} => {
handle_delete_bucket(&garage, bucket_id, bucket_name, api_key).await handle_delete_bucket(&garage, bucket_id, bucket_name, api_key).await
} }
Endpoint::GetBucketLocation { .. } => handle_get_bucket_location(garage), Endpoint::GetBucketLocation {} => handle_get_bucket_location(garage),
Endpoint::GetBucketVersioning { .. } => handle_get_bucket_versioning(), Endpoint::GetBucketVersioning {} => handle_get_bucket_versioning(),
Endpoint::ListObjects { Endpoint::ListObjects {
bucket,
delimiter, delimiter,
encoding_type, encoding_type,
marker, marker,
@ -224,7 +215,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
garage, garage,
&ListObjectsQuery { &ListObjectsQuery {
common: ListQueryCommon { common: ListQueryCommon {
bucket_name: bucket, bucket_name,
bucket_id, bucket_id,
delimiter: delimiter.map(|d| d.to_string()), delimiter: delimiter.map(|d| d.to_string()),
page_size: max_keys.map(|p| min(1000, max(1, p))).unwrap_or(1000), page_size: max_keys.map(|p| min(1000, max(1, p))).unwrap_or(1000),
@ -240,7 +231,6 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
.await .await
} }
Endpoint::ListObjectsV2 { Endpoint::ListObjectsV2 {
bucket,
delimiter, delimiter,
encoding_type, encoding_type,
max_keys, max_keys,
@ -255,7 +245,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
garage, garage,
&ListObjectsQuery { &ListObjectsQuery {
common: ListQueryCommon { common: ListQueryCommon {
bucket_name: bucket, bucket_name,
bucket_id, bucket_id,
delimiter: delimiter.map(|d| d.to_string()), delimiter: delimiter.map(|d| d.to_string()),
page_size: max_keys.map(|p| min(1000, max(1, p))).unwrap_or(1000), page_size: max_keys.map(|p| min(1000, max(1, p))).unwrap_or(1000),
@ -277,7 +267,6 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
} }
} }
Endpoint::ListMultipartUploads { Endpoint::ListMultipartUploads {
bucket,
delimiter, delimiter,
encoding_type, encoding_type,
key_marker, key_marker,
@ -289,7 +278,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
garage, garage,
&ListMultipartUploadsQuery { &ListMultipartUploadsQuery {
common: ListQueryCommon { common: ListQueryCommon {
bucket_name: bucket, bucket_name,
bucket_id, bucket_id,
delimiter: delimiter.map(|d| d.to_string()), delimiter: delimiter.map(|d| d.to_string()),
page_size: max_uploads.map(|p| min(1000, max(1, p))).unwrap_or(1000), page_size: max_uploads.map(|p| min(1000, max(1, p))).unwrap_or(1000),
@ -302,14 +291,14 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
) )
.await .await
} }
Endpoint::DeleteObjects { .. } => { Endpoint::DeleteObjects {} => {
handle_delete_objects(garage, bucket_id, req, content_sha256).await handle_delete_objects(garage, bucket_id, req, content_sha256).await
} }
Endpoint::GetBucketWebsite { .. } => handle_get_website(garage, bucket_id).await, Endpoint::GetBucketWebsite {} => handle_get_website(garage, bucket_id).await,
Endpoint::PutBucketWebsite { .. } => { Endpoint::PutBucketWebsite {} => {
handle_put_website(garage, bucket_id, req, content_sha256).await handle_put_website(garage, bucket_id, req, content_sha256).await
} }
Endpoint::DeleteBucketWebsite { .. } => handle_delete_website(garage, bucket_id).await, Endpoint::DeleteBucketWebsite {} => handle_delete_website(garage, bucket_id).await,
endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())),
} }
} }

View file

@ -8,6 +8,17 @@ use hyper::{HeaderMap, Method, Request};
/// This macro is used to generate very repetitive match {} blocks in this module /// This macro is used to generate very repetitive match {} blocks in this module
/// It is _not_ made to be used anywhere else /// It is _not_ made to be used anywhere else
macro_rules! s3_match { macro_rules! s3_match {
(@match $enum:expr , [ $($endpoint:ident,)* ]) => {{
// usage: s3_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] }
// returns true if the variant was one of the listed variants, false otherwise.
use Endpoint::*;
match $enum {
$(
$endpoint { .. } => true,
)*
_ => false
}
}};
(@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{ (@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{
// usage: s3_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] } // usage: s3_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] }
// returns Some(field_value), or None if the variant was not one of the listed variants. // returns Some(field_value), or None if the variant was not one of the listed variants.
@ -19,10 +30,10 @@ macro_rules! s3_match {
_ => None _ => None
} }
}}; }};
(@gen_parser ($keyword:expr, $key:expr, $bucket:expr, $query:expr, $header:expr), (@gen_parser ($keyword:expr, $key:expr, $query:expr, $header:expr),
key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*], key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*],
no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{ no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{
// usage: s3_match {@gen_parser (keyword, key, bucket, query, header), // usage: s3_match {@gen_parser (keyword, key, query, header),
// key: [ // key: [
// SOME_KEYWORD => VariantWithKey, // SOME_KEYWORD => VariantWithKey,
// ... // ...
@ -38,7 +49,6 @@ macro_rules! s3_match {
match ($keyword, !$key.is_empty()){ match ($keyword, !$key.is_empty()){
$( $(
($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($api_k { ($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($api_k {
bucket: $bucket,
key: $key, key: $key,
$($( $($(
$param_k: s3_match!(@@parse_param $query, $conv_k, $param_k), $param_k: s3_match!(@@parse_param $query, $conv_k, $param_k),
@ -47,7 +57,6 @@ macro_rules! s3_match {
)* )*
$( $(
($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk { ($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk {
bucket: $bucket,
$($( $($(
$param_nk: s3_match!(@@parse_param $query, $conv_nk, $param_nk), $param_nk: s3_match!(@@parse_param $query, $conv_nk, $param_nk),
)*)? )*)?
@ -87,7 +96,6 @@ macro_rules! s3_match {
$( $(
$(#[$outer:meta])* $(#[$outer:meta])*
$variant:ident $({ $variant:ident $({
bucket: String,
$($name:ident: $ty:ty,)* $($name:ident: $ty:ty,)*
})?, })?,
)* )*
@ -97,7 +105,6 @@ macro_rules! s3_match {
$( $(
$(#[$outer])* $(#[$outer])*
$variant $({ $variant $({
bucket: String,
$($name: $ty, )* $($name: $ty, )*
})?, })?,
)* )*
@ -108,15 +115,6 @@ macro_rules! s3_match {
$(Endpoint::$variant $({ $($name: _,)* .. })? => stringify!($variant),)* $(Endpoint::$variant $({ $($name: _,)* .. })? => stringify!($variant),)*
} }
} }
/// Get the bucket the request target. Returns None for requests not related to a bucket.
pub fn get_bucket(&self) -> Option<&str> {
match self {
$(
Endpoint::$variant $({ bucket, $($name: _,)* .. })? => s3_match!{@if ($(bucket $($name)*)?) then (Some(bucket)) else (None)},
)*
}
}
} }
}; };
(@if ($($cond:tt)+) then ($($then:tt)*) else ($($else:tt)*)) => { (@if ($($cond:tt)+) then ($($then:tt)*) else ($($else:tt)*)) => {
@ -138,215 +136,158 @@ s3_match! {@func
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Endpoint { pub enum Endpoint {
AbortMultipartUpload { AbortMultipartUpload {
bucket: String,
key: String, key: String,
upload_id: String, upload_id: String,
}, },
CompleteMultipartUpload { CompleteMultipartUpload {
bucket: String,
key: String, key: String,
upload_id: String, upload_id: String,
}, },
CopyObject { CopyObject {
bucket: String,
key: String, key: String,
}, },
CreateBucket { CreateBucket {
bucket: String,
}, },
CreateMultipartUpload { CreateMultipartUpload {
bucket: String,
key: String, key: String,
}, },
DeleteBucket { DeleteBucket {
bucket: String,
}, },
DeleteBucketAnalyticsConfiguration { DeleteBucketAnalyticsConfiguration {
bucket: String,
id: String, id: String,
}, },
DeleteBucketCors { DeleteBucketCors {
bucket: String,
}, },
DeleteBucketEncryption { DeleteBucketEncryption {
bucket: String,
}, },
DeleteBucketIntelligentTieringConfiguration { DeleteBucketIntelligentTieringConfiguration {
bucket: String,
id: String, id: String,
}, },
DeleteBucketInventoryConfiguration { DeleteBucketInventoryConfiguration {
bucket: String,
id: String, id: String,
}, },
DeleteBucketLifecycle { DeleteBucketLifecycle {
bucket: String,
}, },
DeleteBucketMetricsConfiguration { DeleteBucketMetricsConfiguration {
bucket: String,
id: String, id: String,
}, },
DeleteBucketOwnershipControls { DeleteBucketOwnershipControls {
bucket: String,
}, },
DeleteBucketPolicy { DeleteBucketPolicy {
bucket: String,
}, },
DeleteBucketReplication { DeleteBucketReplication {
bucket: String,
}, },
DeleteBucketTagging { DeleteBucketTagging {
bucket: String,
}, },
DeleteBucketWebsite { DeleteBucketWebsite {
bucket: String,
}, },
DeleteObject { DeleteObject {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
DeleteObjects { DeleteObjects {
bucket: String,
}, },
DeleteObjectTagging { DeleteObjectTagging {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
DeletePublicAccessBlock { DeletePublicAccessBlock {
bucket: String,
}, },
GetBucketAccelerateConfiguration { GetBucketAccelerateConfiguration {
bucket: String,
}, },
GetBucketAcl { GetBucketAcl {
bucket: String,
}, },
GetBucketAnalyticsConfiguration { GetBucketAnalyticsConfiguration {
bucket: String,
id: String, id: String,
}, },
GetBucketCors { GetBucketCors {
bucket: String,
}, },
GetBucketEncryption { GetBucketEncryption {
bucket: String,
}, },
GetBucketIntelligentTieringConfiguration { GetBucketIntelligentTieringConfiguration {
bucket: String,
id: String, id: String,
}, },
GetBucketInventoryConfiguration { GetBucketInventoryConfiguration {
bucket: String,
id: String, id: String,
}, },
GetBucketLifecycleConfiguration { GetBucketLifecycleConfiguration {
bucket: String,
}, },
GetBucketLocation { GetBucketLocation {
bucket: String,
}, },
GetBucketLogging { GetBucketLogging {
bucket: String,
}, },
GetBucketMetricsConfiguration { GetBucketMetricsConfiguration {
bucket: String,
id: String, id: String,
}, },
GetBucketNotificationConfiguration { GetBucketNotificationConfiguration {
bucket: String,
}, },
GetBucketOwnershipControls { GetBucketOwnershipControls {
bucket: String,
}, },
GetBucketPolicy { GetBucketPolicy {
bucket: String,
}, },
GetBucketPolicyStatus { GetBucketPolicyStatus {
bucket: String,
}, },
GetBucketReplication { GetBucketReplication {
bucket: String,
}, },
GetBucketRequestPayment { GetBucketRequestPayment {
bucket: String,
}, },
GetBucketTagging { GetBucketTagging {
bucket: String,
}, },
GetBucketVersioning { GetBucketVersioning {
bucket: String,
}, },
GetBucketWebsite { GetBucketWebsite {
bucket: String,
}, },
/// There are actually many more query parameters, used to add headers to the answer. They were /// There are actually many more query parameters, used to add headers to the answer. They were
/// not added here as they are best handled in a dedicated route. /// not added here as they are best handled in a dedicated route.
GetObject { GetObject {
bucket: String,
key: String, key: String,
part_number: Option<u64>, part_number: Option<u64>,
version_id: Option<String>, version_id: Option<String>,
}, },
GetObjectAcl { GetObjectAcl {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
GetObjectLegalHold { GetObjectLegalHold {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
GetObjectLockConfiguration { GetObjectLockConfiguration {
bucket: String,
}, },
GetObjectRetention { GetObjectRetention {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
GetObjectTagging { GetObjectTagging {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
GetObjectTorrent { GetObjectTorrent {
bucket: String,
key: String, key: String,
}, },
GetPublicAccessBlock { GetPublicAccessBlock {
bucket: String,
}, },
HeadBucket { HeadBucket {
bucket: String,
}, },
HeadObject { HeadObject {
bucket: String,
key: String, key: String,
part_number: Option<u64>, part_number: Option<u64>,
version_id: Option<String>, version_id: Option<String>,
}, },
ListBucketAnalyticsConfigurations { ListBucketAnalyticsConfigurations {
bucket: String,
continuation_token: Option<String>, continuation_token: Option<String>,
}, },
ListBucketIntelligentTieringConfigurations { ListBucketIntelligentTieringConfigurations {
bucket: String,
continuation_token: Option<String>, continuation_token: Option<String>,
}, },
ListBucketInventoryConfigurations { ListBucketInventoryConfigurations {
bucket: String,
continuation_token: Option<String>, continuation_token: Option<String>,
}, },
ListBucketMetricsConfigurations { ListBucketMetricsConfigurations {
bucket: String,
continuation_token: Option<String>, continuation_token: Option<String>,
}, },
ListBuckets, ListBuckets,
ListMultipartUploads { ListMultipartUploads {
bucket: String,
delimiter: Option<char>, delimiter: Option<char>,
encoding_type: Option<String>, encoding_type: Option<String>,
key_marker: Option<String>, key_marker: Option<String>,
@ -355,7 +296,6 @@ pub enum Endpoint {
upload_id_marker: Option<String>, upload_id_marker: Option<String>,
}, },
ListObjects { ListObjects {
bucket: String,
delimiter: Option<char>, delimiter: Option<char>,
encoding_type: Option<String>, encoding_type: Option<String>,
marker: Option<String>, marker: Option<String>,
@ -363,7 +303,6 @@ pub enum Endpoint {
prefix: Option<String>, prefix: Option<String>,
}, },
ListObjectsV2 { ListObjectsV2 {
bucket: String,
// This value should always be 2. It is not checked when constructing the struct // This value should always be 2. It is not checked when constructing the struct
list_type: String, list_type: String,
continuation_token: Option<String>, continuation_token: Option<String>,
@ -375,7 +314,6 @@ pub enum Endpoint {
start_after: Option<String>, start_after: Option<String>,
}, },
ListObjectVersions { ListObjectVersions {
bucket: String,
delimiter: Option<char>, delimiter: Option<char>,
encoding_type: Option<String>, encoding_type: Option<String>,
key_marker: Option<String>, key_marker: Option<String>,
@ -384,119 +322,89 @@ pub enum Endpoint {
version_id_marker: Option<String>, version_id_marker: Option<String>,
}, },
ListParts { ListParts {
bucket: String,
key: String, key: String,
max_parts: Option<u64>, max_parts: Option<u64>,
part_number_marker: Option<u64>, part_number_marker: Option<u64>,
upload_id: String, upload_id: String,
}, },
PutBucketAccelerateConfiguration { PutBucketAccelerateConfiguration {
bucket: String,
}, },
PutBucketAcl { PutBucketAcl {
bucket: String,
}, },
PutBucketAnalyticsConfiguration { PutBucketAnalyticsConfiguration {
bucket: String,
id: String, id: String,
}, },
PutBucketCors { PutBucketCors {
bucket: String,
}, },
PutBucketEncryption { PutBucketEncryption {
bucket: String,
}, },
PutBucketIntelligentTieringConfiguration { PutBucketIntelligentTieringConfiguration {
bucket: String,
id: String, id: String,
}, },
PutBucketInventoryConfiguration { PutBucketInventoryConfiguration {
bucket: String,
id: String, id: String,
}, },
PutBucketLifecycleConfiguration { PutBucketLifecycleConfiguration {
bucket: String,
}, },
PutBucketLogging { PutBucketLogging {
bucket: String,
}, },
PutBucketMetricsConfiguration { PutBucketMetricsConfiguration {
bucket: String,
id: String, id: String,
}, },
PutBucketNotificationConfiguration { PutBucketNotificationConfiguration {
bucket: String,
}, },
PutBucketOwnershipControls { PutBucketOwnershipControls {
bucket: String,
}, },
PutBucketPolicy { PutBucketPolicy {
bucket: String,
}, },
PutBucketReplication { PutBucketReplication {
bucket: String,
}, },
PutBucketRequestPayment { PutBucketRequestPayment {
bucket: String,
}, },
PutBucketTagging { PutBucketTagging {
bucket: String,
}, },
PutBucketVersioning { PutBucketVersioning {
bucket: String,
}, },
PutBucketWebsite { PutBucketWebsite {
bucket: String,
}, },
PutObject { PutObject {
bucket: String,
key: String, key: String,
}, },
PutObjectAcl { PutObjectAcl {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
PutObjectLegalHold { PutObjectLegalHold {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
PutObjectLockConfiguration { PutObjectLockConfiguration {
bucket: String,
}, },
PutObjectRetention { PutObjectRetention {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
PutObjectTagging { PutObjectTagging {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
PutPublicAccessBlock { PutPublicAccessBlock {
bucket: String,
}, },
RestoreObject { RestoreObject {
bucket: String,
key: String, key: String,
version_id: Option<String>, version_id: Option<String>,
}, },
SelectObjectContent { SelectObjectContent {
bucket: String,
key: String, key: String,
// This value should always be 2. It is not checked when constructing the struct // This value should always be 2. It is not checked when constructing the struct
select_type: String, select_type: String,
}, },
UploadPart { UploadPart {
bucket: String,
key: String, key: String,
part_number: u64, part_number: u64,
upload_id: String, upload_id: String,
}, },
UploadPartCopy { UploadPartCopy {
bucket: String,
key: String, key: String,
part_number: u64, part_number: u64,
upload_id: String, upload_id: String,
@ -506,12 +414,16 @@ pub enum Endpoint {
impl Endpoint { impl Endpoint {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was /// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header. /// possibly extracted from the Host header.
pub fn from_request<T>(req: &Request<T>, bucket: Option<String>) -> Result<Self, Error> { /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub fn from_request<T>(
req: &Request<T>,
bucket: Option<String>,
) -> Result<(Self, Option<String>), Error> {
let uri = req.uri(); let uri = req.uri();
let path = uri.path().trim_start_matches('/'); let path = uri.path().trim_start_matches('/');
let query = uri.query(); let query = uri.query();
if bucket.is_none() && path.is_empty() { if bucket.is_none() && path.is_empty() {
return Ok(Self::ListBuckets); return Ok((Self::ListBuckets, None));
} }
let (bucket, key) = if let Some(bucket) = bucket { let (bucket, key) = if let Some(bucket) = bucket {
@ -529,29 +441,25 @@ impl Endpoint {
let mut query = QueryParameters::from_query(query.unwrap_or_default())?; let mut query = QueryParameters::from_query(query.unwrap_or_default())?;
let res = match *req.method() { let res = match *req.method() {
Method::GET => Self::from_get(bucket, key, &mut query)?, Method::GET => Self::from_get(key, &mut query)?,
Method::HEAD => Self::from_head(bucket, key, &mut query)?, Method::HEAD => Self::from_head(key, &mut query)?,
Method::POST => Self::from_post(bucket, key, &mut query)?, Method::POST => Self::from_post(key, &mut query)?,
Method::PUT => Self::from_put(bucket, key, &mut query, req.headers())?, Method::PUT => Self::from_put(key, &mut query, req.headers())?,
Method::DELETE => Self::from_delete(bucket, key, &mut query)?, Method::DELETE => Self::from_delete(key, &mut query)?,
_ => return Err(Error::BadRequest("Unknown method".to_owned())), _ => return Err(Error::BadRequest("Unknown method".to_owned())),
}; };
if let Some(message) = query.nonempty_message() { if let Some(message) = query.nonempty_message() {
debug!("Unused query parameter: {}", message) debug!("Unused query parameter: {}", message)
} }
Ok(res) Ok((res, Some(bucket)))
} }
/// Determine which endpoint a request is for, knowing it is a GET. /// Determine which endpoint a request is for, knowing it is a GET.
fn from_get( fn from_get(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
bucket: String,
key: String,
query: &mut QueryParameters<'_>,
) -> Result<Self, Error> {
s3_match! { s3_match! {
@gen_parser @gen_parser
(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None), (query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
key: [ key: [
EMPTY if upload_id => ListParts (query::upload_id, opt_parse::max_parts, opt_parse::part_number_marker), EMPTY if upload_id => ListParts (query::upload_id, opt_parse::max_parts, opt_parse::part_number_marker),
EMPTY => GetObject (query_opt::version_id, opt_parse::part_number), EMPTY => GetObject (query_opt::version_id, opt_parse::part_number),
@ -605,14 +513,10 @@ impl Endpoint {
} }
/// Determine which endpoint a request is for, knowing it is a HEAD. /// Determine which endpoint a request is for, knowing it is a HEAD.
fn from_head( fn from_head(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
bucket: String,
key: String,
query: &mut QueryParameters<'_>,
) -> Result<Self, Error> {
s3_match! { s3_match! {
@gen_parser @gen_parser
(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None), (query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
key: [ key: [
EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id), EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id),
], ],
@ -623,14 +527,10 @@ impl Endpoint {
} }
/// Determine which endpoint a request is for, knowing it is a POST. /// Determine which endpoint a request is for, knowing it is a POST.
fn from_post( fn from_post(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
bucket: String,
key: String,
query: &mut QueryParameters<'_>,
) -> Result<Self, Error> {
s3_match! { s3_match! {
@gen_parser @gen_parser
(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None), (query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
key: [ key: [
EMPTY if upload_id => CompleteMultipartUpload (query::upload_id), EMPTY if upload_id => CompleteMultipartUpload (query::upload_id),
RESTORE => RestoreObject (query_opt::version_id), RESTORE => RestoreObject (query_opt::version_id),
@ -645,14 +545,13 @@ impl Endpoint {
/// Determine which endpoint a request is for, knowing it is a PUT. /// Determine which endpoint a request is for, knowing it is a PUT.
fn from_put( fn from_put(
bucket: String,
key: String, key: String,
query: &mut QueryParameters<'_>, query: &mut QueryParameters<'_>,
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
s3_match! { s3_match! {
@gen_parser @gen_parser
(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, headers), (query.keyword.take().unwrap_or_default().as_ref(), key, query, headers),
key: [ key: [
EMPTY if part_number header "x-amz-copy-source" => UploadPartCopy (parse::part_number, query::upload_id), EMPTY if part_number header "x-amz-copy-source" => UploadPartCopy (parse::part_number, query::upload_id),
EMPTY header "x-amz-copy-source" => CopyObject, EMPTY header "x-amz-copy-source" => CopyObject,
@ -691,14 +590,10 @@ impl Endpoint {
} }
/// Determine which endpoint a request is for, knowing it is a DELETE. /// Determine which endpoint a request is for, knowing it is a DELETE.
fn from_delete( fn from_delete(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
bucket: String,
key: String,
query: &mut QueryParameters<'_>,
) -> Result<Self, Error> {
s3_match! { s3_match! {
@gen_parser @gen_parser
(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None), (query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
key: [ key: [
EMPTY if upload_id => AbortMultipartUpload (query::upload_id), EMPTY if upload_id => AbortMultipartUpload (query::upload_id),
EMPTY => DeleteObject (query_opt::version_id), EMPTY => DeleteObject (query_opt::version_id),
@ -759,16 +654,13 @@ impl Endpoint {
} }
/// Get the kind of authorization which is required to perform the operation. /// Get the kind of authorization which is required to perform the operation.
pub fn authorization_type(&self) -> Authorization<'_> { pub fn authorization_type(&self) -> Authorization {
let bucket = if let Some(bucket) = self.get_bucket() { if let Endpoint::ListBuckets = self {
bucket
} else {
return Authorization::None; return Authorization::None;
}; };
let readonly = s3_match! { let readonly = s3_match! {
@extract @match
self, self,
bucket,
[ [
GetBucketAccelerateConfiguration, GetBucketAccelerateConfiguration,
GetBucketAcl, GetBucketAcl,
@ -810,41 +702,38 @@ impl Endpoint {
ListParts, ListParts,
SelectObjectContent, SelectObjectContent,
] ]
} };
.is_some();
let owner = s3_match! { let owner = s3_match! {
@extract @match
self, self,
bucket,
[ [
DeleteBucket, DeleteBucket,
GetBucketWebsite, GetBucketWebsite,
PutBucketWebsite, PutBucketWebsite,
DeleteBucketWebsite, DeleteBucketWebsite,
] ]
} };
.is_some();
if readonly { if readonly {
Authorization::Read(bucket) Authorization::Read
} else if owner { } else if owner {
Authorization::Owner(bucket) Authorization::Owner
} else { } else {
Authorization::Write(bucket) Authorization::Write
} }
} }
} }
/// What kind of authorization is required to perform a given action /// What kind of authorization is required to perform a given action
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Authorization<'a> { pub enum Authorization {
/// No authorization is required /// No authorization is required
None, None,
/// Having Read permission on bucket .0 is required /// Having Read permission on bucket
Read(&'a str), Read,
/// Having Write permission on bucket .0 is required /// Having Write permission on bucket
Write(&'a str), Write,
/// Having Owner permission on bucket .0 is required /// Having Owner permission on bucket
Owner(&'a str), Owner,
} }
/// This macro is used to generate part of the code in this module. It must be called only one, and /// This macro is used to generate part of the code in this module. It must be called only one, and
@ -985,7 +874,7 @@ mod tests {
uri: &str, uri: &str,
bucket: Option<String>, bucket: Option<String>,
header: Option<(&str, &str)>, header: Option<(&str, &str)>,
) -> Endpoint { ) -> (Endpoint, Option<String>) {
let mut req = Request::builder().method(method).uri(uri); let mut req = Request::builder().method(method).uri(uri);
if let Some((k, v)) = header { if let Some((k, v)) = header {
req = req.header(k, v) req = req.header(k, v)
@ -1000,13 +889,13 @@ mod tests {
$( $(
assert!( assert!(
matches!( matches!(
parse(test_cases!{@actual_method $method}, $uri, Some("my_bucket".to_owned()), None), parse(test_cases!{@actual_method $method}, $uri, Some("my_bucket".to_owned()), None).0,
Endpoint::$variant { .. } Endpoint::$variant { .. }
) )
); );
assert!( assert!(
matches!( matches!(
parse(test_cases!{@actual_method $method}, concat!("/my_bucket", $uri), None, None), parse(test_cases!{@actual_method $method}, concat!("/my_bucket", $uri), None, None).0,
Endpoint::$variant { .. } Endpoint::$variant { .. }
) )
); );
@ -1025,78 +914,82 @@ mod tests {
(@actual_method OWNER_DELETE) => {{ "DELETE" }}; (@actual_method OWNER_DELETE) => {{ "DELETE" }};
(@auth HEAD $uri:expr) => {{ (@auth HEAD $uri:expr) => {{
assert_eq!(parse("HEAD", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("HEAD", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Read("my_bucket")) Authorization::Read)
}}; }};
(@auth GET $uri:expr) => {{ (@auth GET $uri:expr) => {{
assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Read("my_bucket")) Authorization::Read)
}}; }};
(@auth OWNER_GET $uri:expr) => {{ (@auth OWNER_GET $uri:expr) => {{
assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Owner("my_bucket")) Authorization::Owner)
}}; }};
(@auth PUT $uri:expr) => {{ (@auth PUT $uri:expr) => {{
assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Write("my_bucket")) Authorization::Write)
}}; }};
(@auth OWNER_PUT $uri:expr) => {{ (@auth OWNER_PUT $uri:expr) => {{
assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Owner("my_bucket")) Authorization::Owner)
}}; }};
(@auth POST $uri:expr) => {{ (@auth POST $uri:expr) => {{
assert_eq!(parse("POST", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("POST", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Write("my_bucket")) Authorization::Write)
}}; }};
(@auth DELETE $uri:expr) => {{ (@auth DELETE $uri:expr) => {{
assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Write("my_bucket")) Authorization::Write)
}}; }};
(@auth OWNER_DELETE $uri:expr) => {{ (@auth OWNER_DELETE $uri:expr) => {{
assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).authorization_type(), assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).0.authorization_type(),
Authorization::Owner("my_bucket")) Authorization::Owner)
}}; }};
} }
#[test] #[test]
fn test_bucket_extraction() { fn test_bucket_extraction() {
assert_eq!( assert_eq!(
parse("GET", "/my/key", Some("my_bucket".to_owned()), None).get_bucket(), parse("GET", "/my/key", Some("my_bucket".to_owned()), None).1,
parse("GET", "/my_bucket/my/key", None, None).get_bucket() parse("GET", "/my_bucket/my/key", None, None).1
); );
assert_eq!( assert_eq!(
parse("GET", "/my_bucket/my/key", None, None) parse("GET", "/my_bucket/my/key", None, None).1.unwrap(),
.get_bucket()
.unwrap(),
"my_bucket" "my_bucket"
); );
assert!(parse("GET", "/", None, None).get_bucket().is_none()); assert!(parse("GET", "/", None, None).1.is_none());
} }
#[test] #[test]
fn test_key() { fn test_key() {
assert_eq!( assert_eq!(
parse("GET", "/my/key", Some("my_bucket".to_owned()), None).get_key(), parse("GET", "/my/key", Some("my_bucket".to_owned()), None)
parse("GET", "/my_bucket/my/key", None, None).get_key() .0
.get_key(),
parse("GET", "/my_bucket/my/key", None, None).0.get_key()
); );
assert_eq!( assert_eq!(
parse("GET", "/my_bucket/my/key", None, None) parse("GET", "/my_bucket/my/key", None, None)
.0
.get_key() .get_key()
.unwrap(), .unwrap(),
"my/key" "my/key"
); );
assert_eq!( assert_eq!(
parse("GET", "/my_bucket/my/key?acl", None, None) parse("GET", "/my_bucket/my/key?acl", None, None)
.0
.get_key() .get_key()
.unwrap(), .unwrap(),
"my/key" "my/key"
); );
assert!(parse("GET", "/my_bucket/?list-type=2", None, None) assert!(parse("GET", "/my_bucket/?list-type=2", None, None)
.0
.get_key() .get_key()
.is_none()); .is_none());
assert_eq!( assert_eq!(
parse("GET", "/my_bucket/%26%2B%3F%25%C3%A9/something", None, None) parse("GET", "/my_bucket/%26%2B%3F%25%C3%A9/something", None, None)
.0
.get_key() .get_key()
.unwrap(), .unwrap(),
"&+?%é/something" "&+?%é/something"
@ -1268,11 +1161,11 @@ mod tests {
); );
// no bucket, won't work with the rest of the test suite // no bucket, won't work with the rest of the test suite
assert!(matches!( assert!(matches!(
parse("GET", "/", None, None), parse("GET", "/", None, None).0,
Endpoint::ListBuckets { .. } Endpoint::ListBuckets { .. }
)); ));
assert!(matches!( assert!(matches!(
parse("GET", "/", None, None).authorization_type(), parse("GET", "/", None, None).0.authorization_type(),
Authorization::None Authorization::None
)); ));
@ -1283,16 +1176,8 @@ mod tests {
"/Key+", "/Key+",
Some("my_bucket".to_owned()), Some("my_bucket".to_owned()),
Some(("x-amz-copy-source", "some/key")) Some(("x-amz-copy-source", "some/key"))
), )
Endpoint::CopyObject { .. } .0,
));
assert!(matches!(
parse(
"PUT",
"/my_bucket/Key+",
None,
Some(("x-amz-copy-source", "some/key"))
),
Endpoint::CopyObject { .. } Endpoint::CopyObject { .. }
)); ));
assert!(matches!( assert!(matches!(
@ -1302,8 +1187,19 @@ mod tests {
None, None,
Some(("x-amz-copy-source", "some/key")) Some(("x-amz-copy-source", "some/key"))
) )
.0,
Endpoint::CopyObject { .. }
));
assert!(matches!(
parse(
"PUT",
"/my_bucket/Key+",
None,
Some(("x-amz-copy-source", "some/key"))
)
.0
.authorization_type(), .authorization_type(),
Authorization::Write("my_bucket") Authorization::Write
)); ));
// require a header // require a header
@ -1313,16 +1209,8 @@ mod tests {
"/Key+?partNumber=2&uploadId=UploadId", "/Key+?partNumber=2&uploadId=UploadId",
Some("my_bucket".to_owned()), Some("my_bucket".to_owned()),
Some(("x-amz-copy-source", "some/key")) Some(("x-amz-copy-source", "some/key"))
), )
Endpoint::UploadPartCopy { .. } .0,
));
assert!(matches!(
parse(
"PUT",
"/my_bucket/Key+?partNumber=2&uploadId=UploadId",
None,
Some(("x-amz-copy-source", "some/key"))
),
Endpoint::UploadPartCopy { .. } Endpoint::UploadPartCopy { .. }
)); ));
assert!(matches!( assert!(matches!(
@ -1332,8 +1220,19 @@ mod tests {
None, None,
Some(("x-amz-copy-source", "some/key")) Some(("x-amz-copy-source", "some/key"))
) )
.0,
Endpoint::UploadPartCopy { .. }
));
assert!(matches!(
parse(
"PUT",
"/my_bucket/Key+?partNumber=2&uploadId=UploadId",
None,
Some(("x-amz-copy-source", "some/key"))
)
.0
.authorization_type(), .authorization_type(),
Authorization::Write("my_bucket") Authorization::Write
)); ));
// POST request, but with GET semantic for permissions purpose // POST request, but with GET semantic for permissions purpose
@ -1343,17 +1242,19 @@ mod tests {
"/{Key+}?select&select-type=2", "/{Key+}?select&select-type=2",
Some("my_bucket".to_owned()), Some("my_bucket".to_owned()),
None None
), )
.0,
Endpoint::SelectObjectContent { .. } Endpoint::SelectObjectContent { .. }
)); ));
assert!(matches!( assert!(matches!(
parse("POST", "/my_bucket/{Key+}?select&select-type=2", None, None), parse("POST", "/my_bucket/{Key+}?select&select-type=2", None, None).0,
Endpoint::SelectObjectContent { .. } Endpoint::SelectObjectContent { .. }
)); ));
assert!(matches!( assert!(matches!(
parse("POST", "/my_bucket/{Key+}?select&select-type=2", None, None) parse("POST", "/my_bucket/{Key+}?select&select-type=2", None, None)
.0
.authorization_type(), .authorization_type(),
Authorization::Read("my_bucket") Authorization::Read
)); ));
} }
} }