diff --git a/src/api/api_server.rs b/src/api/api_server.rs index db5da8cc..41ae6857 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -123,6 +123,7 @@ async fn handler_inner(garage: Arc, req: Request) -> Result api_key.allow_read(&bucket_id), Authorization::Write(_) => api_key.allow_write(&bucket_id), + Authorization::Owner(_) => api_key.allow_owner(&bucket_id), _ => unreachable!(), }; @@ -199,9 +200,9 @@ async fn handler_inner(garage: Arc, req: Request) -> Result Err(Error::Forbidden( - "Cannot delete buckets using S3 api, please talk to Garage directly".into(), - )), + Endpoint::DeleteBucket { .. } => { + handle_delete_bucket(&garage, bucket_id, bucket_name, api_key).await + } Endpoint::GetBucketLocation { .. } => handle_get_bucket_location(garage), Endpoint::GetBucketVersioning { .. } => handle_get_bucket_versioning(), Endpoint::ListObjects { diff --git a/src/api/s3_bucket.rs b/src/api/s3_bucket.rs index 23f43317..ea7e0b8c 100644 --- a/src/api/s3_bucket.rs +++ b/src/api/s3_bucket.rs @@ -1,14 +1,14 @@ use std::collections::HashMap; use std::sync::Arc; -use hyper::{Body, Request, Response}; +use hyper::{Body, Request, Response, StatusCode}; use garage_model::bucket_alias_table::*; use garage_model::bucket_table::Bucket; use garage_model::garage::Garage; use garage_model::key_table::Key; use garage_model::permission::BucketKeyPerm; -use garage_table::util::EmptyKey; +use garage_table::util::*; use garage_util::crdt::*; use garage_util::data::*; use garage_util::time::*; @@ -187,6 +187,100 @@ pub async fn handle_create_bucket( .unwrap()) } +pub async fn handle_delete_bucket( + garage: &Garage, + bucket_id: Uuid, + bucket_name: String, + api_key: Key, +) -> Result, Error> { + let key_params = api_key + .params() + .ok_or_internal_error("Key should not be deleted at this point")?; + + let is_local_alias = matches!(key_params.local_aliases.get(&bucket_name), Some(Some(_))); + + let mut bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + let bucket_state = bucket.state.as_option().unwrap(); + + // If the bucket has no other aliases, this is a true deletion. + // Otherwise, it is just an alias removal. + + let has_other_global_aliases = bucket_state + .aliases + .items() + .iter() + .filter(|(_, _, active)| *active) + .any(|(n, _, _)| is_local_alias || (*n != bucket_name)); + + let has_other_local_aliases = bucket_state + .local_aliases + .items() + .iter() + .filter(|(_, _, active)| *active) + .any(|((k, n), _, _)| !is_local_alias || *n != bucket_name || *k != api_key.key_id); + + if !has_other_global_aliases && !has_other_local_aliases { + // Delete bucket + + // Check bucket is empty + let objects = garage + .object_table + .get_range(&bucket_id, None, Some(DeletedFilter::NotDeleted), 10) + .await?; + if !objects.is_empty() { + return Err(Error::BadRequest(format!( + "Bucket {} is not empty", + bucket_name + ))); + } + + // --- done checking, now commit --- + // 1. delete bucket alias + if is_local_alias { + garage + .bucket_helper() + .unset_local_bucket_alias(bucket_id, &api_key.key_id, &bucket_name) + .await?; + } else { + garage + .bucket_helper() + .unset_global_bucket_alias(bucket_id, &bucket_name) + .await?; + } + + // 2. delete authorization from keys that had access + for (key_id, _) in bucket.authorized_keys() { + garage + .bucket_helper() + .set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS) + .await?; + } + + // 3. delete bucket + bucket.state = Deletable::delete(); + garage.bucket_table.insert(&bucket).await?; + } else if is_local_alias { + // Just unalias + garage + .bucket_helper() + .unset_local_bucket_alias(bucket_id, &api_key.key_id, &bucket_name) + .await?; + } else { + // Just unalias (but from global namespace) + garage + .bucket_helper() + .unset_global_bucket_alias(bucket_id, &bucket_name) + .await?; + } + + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?) +} + fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option> { // Returns None if invalid data // Returns Some(None) if no location constraint is given diff --git a/src/api/s3_router.rs b/src/api/s3_router.rs index 4ce1d238..f32aed30 100644 --- a/src/api/s3_router.rs +++ b/src/api/s3_router.rs @@ -789,7 +789,6 @@ impl Endpoint { GetBucketRequestPayment, GetBucketTagging, GetBucketVersioning, - GetBucketWebsite, GetObject, GetObjectAcl, GetObjectLegalHold, @@ -813,8 +812,22 @@ impl Endpoint { ] } .is_some(); + let owner = s3_match! { + @extract + self, + bucket, + [ + DeleteBucket, + GetBucketWebsite, + PutBucketWebsite, + DeleteBucketWebsite, + ] + } + .is_some(); if readonly { Authorization::Read(bucket) + } else if owner { + Authorization::Owner(bucket) } else { Authorization::Write(bucket) } @@ -830,6 +843,8 @@ pub enum Authorization<'a> { Read(&'a str), /// Having Write permission on bucket .0 is required Write(&'a str), + /// Having Owner permission on bucket .0 is required + Owner(&'a str), } /// This macro is used to generate part of the code in this module. It must be called only one, and