diff --git a/doc/book/src/reference_manual/s3_compatibility.md b/doc/book/src/reference_manual/s3_compatibility.md index fd17f28d..5f44de78 100644 --- a/doc/book/src/reference_manual/s3_compatibility.md +++ b/doc/book/src/reference_manual/s3_compatibility.md @@ -28,9 +28,9 @@ All APIs that are not mentionned are not implemented and will return a 400 bad r | AbortMultipartUpload | Implemented | | CompleteMultipartUpload | Implemented | | CopyObject | Implemented | -| CreateBucket | Unsupported, stub (see below) | +| CreateBucket | Implemented | | CreateMultipartUpload | Implemented | -| DeleteBucket | Unsupported (see below) | +| DeleteBucket | Implemented | | DeleteBucketWebsite | Implemented | | DeleteObject | Implemented | | DeleteObjects | Implemented | @@ -48,11 +48,6 @@ All APIs that are not mentionned are not implemented and will return a 400 bad r | UploadPart | Implemented | - -- **CreateBucket:** Garage does not yet accept creating buckets or giving access using API calls, it has to be done using the CLI tools. CreateBucket will return a 200 if the bucket exists and user has write access, and a 403 Forbidden in all other cases. - -- **DeleteBucket:** Garage does not yet accept deleting buckets using API calls, it has to be done using the CLI tools. This request will return a 403 Forbidden. - - **GetBucketVersioning:** Stub implementation (Garage does not yet support versionning so this always returns "versionning not enabled"). diff --git a/doc/book/src/working_documents/compatibility_target.md b/doc/book/src/working_documents/compatibility_target.md index 6225532e..d9027ccb 100644 --- a/doc/book/src/working_documents/compatibility_target.md +++ b/doc/book/src/working_documents/compatibility_target.md @@ -9,8 +9,8 @@ your motivations for doing so in the PR message. | **S-tier** (high priority) | | | | HeadBucket | | | GetBucketLocation | -| | [*CreateBucket*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/97) | -| | [*DeleteBucket*](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/97) | +| | CreateBucket | +| | DeleteBucket | | | ListBuckets | | | ListObjects | | | ListObjectsV2 | diff --git a/src/api/api_server.rs b/src/api/api_server.rs index f5ebed37..41aa0046 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -107,6 +107,12 @@ async fn handler_inner(garage: Arc, req: Request) -> Result return handle_request_without_bucket(garage, req, api_key, endpoint).await, @@ -118,6 +124,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!(), }; @@ -188,27 +195,15 @@ async fn handler_inner(garage: Arc, req: Request) -> Result { - debug!( - "Body: {}", - std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?) - .unwrap_or("") - ); - let empty_body: Body = Body::from(vec![]); - let response = Response::builder() - .header("Location", format!("/{}", bucket)) - .body(empty_body) - .unwrap(); - Ok(response) - } + Endpoint::CreateBucket { .. } => unreachable!(), Endpoint::HeadBucket { .. } => { let empty_body: Body = Body::from(vec![]); let response = Response::builder().body(empty_body).unwrap(); Ok(response) } - Endpoint::DeleteBucket { .. } => 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 { @@ -303,7 +298,7 @@ async fn resolve_bucket( let api_key_params = api_key .state .as_option() - .ok_or_else(|| Error::Forbidden("Operation is not allowed for this key.".to_string()))?; + .ok_or_internal_error("Key should not be deleted at this point")?; if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) { Ok(*bucket_id) @@ -312,7 +307,7 @@ async fn resolve_bucket( .bucket_helper() .resolve_global_bucket_name(bucket_name) .await? - .ok_or(Error::NotFound)?) + .ok_or(Error::NoSuchBucket)?) } } diff --git a/src/api/error.rs b/src/api/error.rs index d6d4a1d7..c19c4f3b 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -35,8 +35,24 @@ pub enum Error { AuthorizationHeaderMalformed(String), /// The object requested don't exists - #[error(display = "Not found")] - NotFound, + #[error(display = "Key not found")] + NoSuchKey, + + /// The bucket requested don't exists + #[error(display = "Bucket not found")] + NoSuchBucket, + + /// The multipart upload requested don't exists + #[error(display = "Upload not found")] + NoSuchUpload, + + /// Tried to create a bucket that already exist + #[error(display = "Bucket already exists")] + BucketAlreadyExists, + + /// Tried to delete a non-empty bucket + #[error(display = "Tried to delete a non-empty bucket")] + BucketNotEmpty, // Category: bad request /// The request contained an invalid UTF-8 sequence in its path or in other parameters @@ -97,7 +113,8 @@ impl Error { /// Get the HTTP status code that best represents the meaning of the error for the client pub fn http_status_code(&self) -> StatusCode { match self { - Error::NotFound => StatusCode::NOT_FOUND, + Error::NoSuchKey | Error::NoSuchBucket | Error::NoSuchUpload => StatusCode::NOT_FOUND, + Error::BucketNotEmpty | Error::BucketAlreadyExists => StatusCode::CONFLICT, Error::Forbidden(_) => StatusCode::FORBIDDEN, Error::InternalError( GarageError::Timeout @@ -115,9 +132,14 @@ impl Error { pub fn aws_code(&self) -> &'static str { match self { - Error::NotFound => "NoSuchKey", + Error::NoSuchKey => "NoSuchKey", + Error::NoSuchBucket => "NoSuchBucket", + Error::NoSuchUpload => "NoSuchUpload", + Error::BucketAlreadyExists => "BucketAlreadyExists", + Error::BucketNotEmpty => "BucketNotEmpty", Error::Forbidden(_) => "AccessDenied", Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed", + Error::NotImplemented(_) => "NotImplemented", Error::InternalError( GarageError::Timeout | GarageError::RemoteError(_) diff --git a/src/api/s3_bucket.rs b/src/api/s3_bucket.rs index 27208ffa..425d2998 100644 --- a/src/api/s3_bucket.rs +++ b/src/api/s3_bucket.rs @@ -1,16 +1,21 @@ use std::collections::HashMap; use std::sync::Arc; -use hyper::{Body, 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_table::util::EmptyKey; +use garage_model::permission::BucketKeyPerm; +use garage_table::util::*; use garage_util::crdt::*; +use garage_util::data::*; use garage_util::time::*; use crate::error::*; use crate::s3_xml; +use crate::signature::verify_signed_content; pub fn handle_get_bucket_location(garage: Arc) -> Result, Error> { let loc = s3_xml::LocationConstraint { @@ -50,7 +55,7 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result>(); @@ -105,3 +110,239 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result, + content_sha256: Option, + api_key: Key, + bucket_name: String, +) -> Result, Error> { + let body = hyper::body::to_bytes(req.into_body()).await?; + verify_signed_content(content_sha256, &body[..])?; + + let cmd = + parse_create_bucket_xml(&body[..]).ok_or_bad_request("Invalid create bucket XML query")?; + + if let Some(location_constraint) = cmd { + if location_constraint != garage.config.s3_api.s3_region { + return Err(Error::BadRequest(format!( + "Cannot satisfy location constraint `{}`: buckets can only be created in region `{}`", + location_constraint, + garage.config.s3_api.s3_region + ))); + } + } + + let key_params = api_key + .params() + .ok_or_internal_error("Key should not be deleted at this point")?; + + let existing_bucket = if let Some(Some(bucket_id)) = key_params.local_aliases.get(&bucket_name) + { + Some(*bucket_id) + } else { + garage + .bucket_helper() + .resolve_global_bucket_name(&bucket_name) + .await? + }; + + if let Some(bucket_id) = existing_bucket { + // Check we have write or owner permission on the bucket, + // in that case it's fine, return 200 OK, bucket exists; + // otherwise return a forbidden error. + let kp = api_key.bucket_permissions(&bucket_id); + if !(kp.allow_write || kp.allow_owner) { + return Err(Error::BucketAlreadyExists); + } + } else { + // Create the bucket! + if !is_valid_bucket_name(&bucket_name) { + return Err(Error::BadRequest(format!( + "{}: {}", + bucket_name, INVALID_BUCKET_NAME_MESSAGE + ))); + } + + let bucket = Bucket::new(); + garage.bucket_table.insert(&bucket).await?; + + garage + .bucket_helper() + .set_bucket_key_permissions(bucket.id, &api_key.key_id, BucketKeyPerm::ALL_PERMISSIONS) + .await?; + + garage + .bucket_helper() + .set_local_bucket_alias(bucket.id, &api_key.key_id, &bucket_name) + .await?; + } + + Ok(Response::builder() + .header("Location", format!("/{}", bucket_name)) + .body(Body::empty()) + .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::BucketNotEmpty); + } + + // --- 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 + // Returns Some(Some("xxxx")) where xxxx is the given location constraint + + let xml_str = std::str::from_utf8(xml_bytes).ok()?; + if xml_str.trim_matches(char::is_whitespace).is_empty() { + return Some(None); + } + + let xml = roxmltree::Document::parse(xml_str).ok()?; + + let cbc = xml.root().first_child()?; + if !cbc.has_tag_name("CreateBucketConfiguration") { + return None; + } + + let mut ret = None; + for item in cbc.children() { + println!("{:?}", item); + if item.has_tag_name("LocationConstraint") { + if ret != None { + return None; + } + ret = Some(item.text()?.to_string()); + } else if !item.is_text() { + return None; + } + } + + Some(ret) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_bucket() { + assert_eq!(parse_create_bucket_xml(br#""#), Some(None)); + assert_eq!( + parse_create_bucket_xml( + br#" + + + "# + ), + Some(None) + ); + assert_eq!( + parse_create_bucket_xml( + br#" + + Europe + + "# + ), + Some(Some("Europe".into())) + ); + assert_eq!( + parse_create_bucket_xml( + br#" + + + "# + ), + None + ); + } +} diff --git a/src/api/s3_copy.rs b/src/api/s3_copy.rs index 4ede8230..7952dae8 100644 --- a/src/api/s3_copy.rs +++ b/src/api/s3_copy.rs @@ -27,14 +27,14 @@ pub async fn handle_copy( .object_table .get(&source_bucket_id, &source_key.to_string()) .await? - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchKey)?; let source_last_v = source_object .versions() .iter() .rev() .find(|v| v.is_complete()) - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchKey)?; let source_last_state = match &source_last_v.state { ObjectVersionState::Complete(x) => x, @@ -47,7 +47,7 @@ pub async fn handle_copy( // Implement x-amz-metadata-directive: REPLACE let old_meta = match source_last_state { ObjectVersionData::DeleteMarker => { - return Err(Error::NotFound); + return Err(Error::NoSuchKey); } ObjectVersionData::Inline(meta, _bytes) => meta, ObjectVersionData::FirstBlock(meta, _fbh) => meta, @@ -88,7 +88,7 @@ pub async fn handle_copy( .version_table .get(&source_last_v.uuid, &EmptyKey) .await?; - let source_version = source_version.ok_or(Error::NotFound)?; + let source_version = source_version.ok_or(Error::NoSuchKey)?; // Write an "uploading" marker in Object table // This holds a reference to the object in the Version table diff --git a/src/api/s3_delete.rs b/src/api/s3_delete.rs index 1976139b..9e267490 100644 --- a/src/api/s3_delete.rs +++ b/src/api/s3_delete.rs @@ -21,7 +21,7 @@ async fn handle_delete_internal( .object_table .get(&bucket_id, &key.to_string()) .await? - .ok_or(Error::NotFound)?; // No need to delete + .ok_or(Error::NoSuchKey)?; // No need to delete let interesting_versions = object.versions().iter().filter(|v| { !matches!( @@ -40,7 +40,7 @@ async fn handle_delete_internal( timestamp = std::cmp::max(timestamp, v.timestamp + 1); } - let deleted_version = version_to_delete.ok_or(Error::NotFound)?; + let deleted_version = version_to_delete.ok_or(Error::NoSuchKey)?; let version_uuid = gen_uuid(); diff --git a/src/api/s3_get.rs b/src/api/s3_get.rs index 269a3fa8..67ab2b59 100644 --- a/src/api/s3_get.rs +++ b/src/api/s3_get.rs @@ -92,14 +92,14 @@ pub async fn handle_head( .object_table .get(&bucket_id, &key.to_string()) .await? - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchKey)?; let version = object .versions() .iter() .rev() .find(|v| v.is_data()) - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchKey)?; let version_meta = match &version.state { ObjectVersionState::Complete(ObjectVersionData::Inline(meta, _)) => meta, @@ -131,21 +131,21 @@ pub async fn handle_get( .object_table .get(&bucket_id, &key.to_string()) .await? - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchKey)?; let last_v = object .versions() .iter() .rev() .find(|v| v.is_complete()) - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchKey)?; let last_v_data = match &last_v.state { ObjectVersionState::Complete(x) => x, _ => unreachable!(), }; let last_v_meta = match last_v_data { - ObjectVersionData::DeleteMarker => return Err(Error::NotFound), + ObjectVersionData::DeleteMarker => return Err(Error::NoSuchKey), ObjectVersionData::Inline(meta, _) => meta, ObjectVersionData::FirstBlock(meta, _) => meta, }; @@ -196,7 +196,7 @@ pub async fn handle_get( let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey); let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?; - let version = version.ok_or(Error::NotFound)?; + let version = version.ok_or(Error::NoSuchKey)?; let mut blocks = version .blocks @@ -261,7 +261,7 @@ async fn handle_get_range( let version = garage.version_table.get(&version.uuid, &EmptyKey).await?; let version = match version { Some(v) => v, - None => return Err(Error::NotFound), + None => return Err(Error::NoSuchKey), }; // We will store here the list of blocks that have an intersection with the requested diff --git a/src/api/s3_put.rs b/src/api/s3_put.rs index 152e59b4..bb92c252 100644 --- a/src/api/s3_put.rs +++ b/src/api/s3_put.rs @@ -382,7 +382,7 @@ pub async fn handle_put_part( .iter() .any(|v| v.uuid == version_uuid && v.is_uploading()) { - return Err(Error::NotFound); + return Err(Error::NoSuchUpload); } // Copy block to store @@ -449,15 +449,15 @@ pub async fn handle_complete_multipart_upload( garage.version_table.get(&version_uuid, &EmptyKey), )?; - let object = object.ok_or_else(|| Error::BadRequest("Object not found".to_string()))?; + let object = object.ok_or(Error::NoSuchKey)?; let mut object_version = object .versions() .iter() .find(|v| v.uuid == version_uuid && v.is_uploading()) .cloned() - .ok_or_else(|| Error::BadRequest("Version not found".to_string()))?; + .ok_or(Error::NoSuchUpload)?; - let version = version.ok_or_else(|| Error::BadRequest("Version not found".to_string()))?; + let version = version.ok_or(Error::NoSuchKey)?; if version.blocks.is_empty() { return Err(Error::BadRequest("No data was uploaded".to_string())); } @@ -538,14 +538,14 @@ pub async fn handle_abort_multipart_upload( .object_table .get(&bucket_id, &key.to_string()) .await?; - let object = object.ok_or_else(|| Error::BadRequest("Object not found".to_string()))?; + let object = object.ok_or(Error::NoSuchKey)?; let object_version = object .versions() .iter() .find(|v| v.uuid == version_uuid && v.is_uploading()); let mut object_version = match object_version { - None => return Err(Error::NotFound), + None => return Err(Error::NoSuchUpload), Some(x) => x.clone(), }; @@ -611,9 +611,9 @@ pub(crate) fn get_headers(req: &Request) -> Result Result { - let id_bin = hex::decode(id).ok_or_bad_request("Invalid upload ID")?; + let id_bin = hex::decode(id).map_err(|_| Error::NoSuchUpload)?; if id_bin.len() != 32 { - return None.ok_or_bad_request("Invalid upload ID"); + return Err(Error::NoSuchUpload); } let mut uuid = [0u8; 32]; uuid.copy_from_slice(&id_bin[..]); diff --git a/src/api/s3_router.rs b/src/api/s3_router.rs index 4ce1d238..234f77f0 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 @@ -985,13 +1000,13 @@ mod tests { $( assert!( matches!( - parse(stringify!($method), $uri, Some("my_bucket".to_owned()), None), + parse(test_cases!{@actual_method $method}, $uri, Some("my_bucket".to_owned()), None), Endpoint::$variant { .. } ) ); assert!( matches!( - parse(stringify!($method), concat!("/my_bucket", $uri), None, None), + parse(test_cases!{@actual_method $method}, concat!("/my_bucket", $uri), None, None), Endpoint::$variant { .. } ) ); @@ -999,6 +1014,16 @@ mod tests { test_cases!{@auth $method $uri} )* }}; + + (@actual_method HEAD) => {{ "HEAD" }}; + (@actual_method GET) => {{ "GET" }}; + (@actual_method OWNER_GET) => {{ "GET" }}; + (@actual_method PUT) => {{ "PUT" }}; + (@actual_method OWNER_PUT) => {{ "PUT" }}; + (@actual_method POST) => {{ "POST" }}; + (@actual_method DELETE) => {{ "DELETE" }}; + (@actual_method OWNER_DELETE) => {{ "DELETE" }}; + (@auth HEAD $uri:expr) => {{ assert_eq!(parse("HEAD", concat!("/my_bucket", $uri), None, None).authorization_type(), Authorization::Read("my_bucket")) @@ -1007,10 +1032,18 @@ mod tests { assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).authorization_type(), Authorization::Read("my_bucket")) }}; + (@auth OWNER_GET $uri:expr) => {{ + assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).authorization_type(), + Authorization::Owner("my_bucket")) + }}; (@auth PUT $uri:expr) => {{ assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).authorization_type(), Authorization::Write("my_bucket")) }}; + (@auth OWNER_PUT $uri:expr) => {{ + assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).authorization_type(), + Authorization::Owner("my_bucket")) + }}; (@auth POST $uri:expr) => {{ assert_eq!(parse("POST", concat!("/my_bucket", $uri), None, None).authorization_type(), Authorization::Write("my_bucket")) @@ -1019,6 +1052,10 @@ mod tests { assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).authorization_type(), Authorization::Write("my_bucket")) }}; + (@auth OWNER_DELETE $uri:expr) => {{ + assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).authorization_type(), + Authorization::Owner("my_bucket")) + }}; } #[test] @@ -1094,7 +1131,7 @@ mod tests { PUT "/" => CreateBucket POST "/example-object?uploads" => CreateMultipartUpload POST "/{Key+}?uploads" => CreateMultipartUpload - DELETE "/" => DeleteBucket + OWNER_DELETE "/" => DeleteBucket DELETE "/?analytics&id=list1" => DeleteBucketAnalyticsConfiguration DELETE "/?analytics&id=Id" => DeleteBucketAnalyticsConfiguration DELETE "/?cors" => DeleteBucketCors @@ -1109,7 +1146,7 @@ mod tests { DELETE "/?policy" => DeleteBucketPolicy DELETE "/?replication" => DeleteBucketReplication DELETE "/?tagging" => DeleteBucketTagging - DELETE "/?website" => DeleteBucketWebsite + OWNER_DELETE "/?website" => DeleteBucketWebsite DELETE "/my-second-image.jpg" => DeleteObject DELETE "/my-third-image.jpg?versionId=UIORUnfndfiufdisojhr398493jfdkjFJjkndnqUifhnw89493jJFJ" => DeleteObject DELETE "/Key+?versionId=VersionId" => DeleteObject @@ -1138,7 +1175,7 @@ mod tests { GET "/?requestPayment" => GetBucketRequestPayment GET "/?tagging" => GetBucketTagging GET "/?versioning" => GetBucketVersioning - GET "/?website" => GetBucketWebsite + OWNER_GET "/?website" => GetBucketWebsite GET "/my-image.jpg" => GetObject GET "/myObject?versionId=3/L4kqtJlcpXroDTDmpUMLUo" => GetObject GET "/Junk3.txt?response-cache-control=No-cache&response-content-disposition=attachment%3B%20filename%3Dtesting.txt&response-content-encoding=x-gzip&response-content-language=mi%2C%20en&response-expires=Thu%2C%2001%20Dec%201994%2016:00:00%20GMT" => GetObject @@ -1212,7 +1249,7 @@ mod tests { PUT "/?requestPayment" => PutBucketRequestPayment PUT "/?tagging" => PutBucketTagging PUT "/?versioning" => PutBucketVersioning - PUT "/?website" => PutBucketWebsite + OWNER_PUT "/?website" => PutBucketWebsite PUT "/my-image.jpg" => PutObject PUT "/Key+" => PutObject PUT "/my-image.jpg?acl" => PutObjectAcl diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs index e141e449..85d7c261 100644 --- a/src/api/s3_website.rs +++ b/src/api/s3_website.rs @@ -22,7 +22,7 @@ pub async fn handle_delete_website( .bucket_table .get(&EmptyKey, &bucket_id) .await? - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchBucket)?; if let crdt::Deletable::Present(param) = &mut bucket.state { param.website_config.update(None); @@ -50,7 +50,7 @@ pub async fn handle_put_website( .bucket_table .get(&EmptyKey, &bucket_id) .await? - .ok_or(Error::NotFound)?; + .ok_or(Error::NoSuchBucket)?; let conf: WebsiteConfiguration = from_reader(&body as &[u8])?; conf.validate()?; diff --git a/src/garage/admin.rs b/src/garage/admin.rs index a682075f..f315c4dc 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -429,6 +429,8 @@ impl AdminRpcHandler { KeyOperation::New(query) => self.handle_create_key(query).await, KeyOperation::Rename(query) => self.handle_rename_key(query).await, KeyOperation::Delete(query) => self.handle_delete_key(query).await, + KeyOperation::Allow(query) => self.handle_allow_key(query).await, + KeyOperation::Deny(query) => self.handle_deny_key(query).await, KeyOperation::Import(query) => self.handle_import_key(query).await, } } @@ -523,6 +525,32 @@ impl AdminRpcHandler { ))) } + async fn handle_allow_key(&self, query: &KeyPermOpt) -> Result { + let mut key = self + .garage + .bucket_helper() + .get_existing_matching_key(&query.key_pattern) + .await?; + if query.create_bucket { + key.params_mut().unwrap().allow_create_bucket.update(true); + } + self.garage.key_table.insert(&key).await?; + self.key_info_result(key).await + } + + async fn handle_deny_key(&self, query: &KeyPermOpt) -> Result { + let mut key = self + .garage + .bucket_helper() + .get_existing_matching_key(&query.key_pattern) + .await?; + if query.create_bucket { + key.params_mut().unwrap().allow_create_bucket.update(false); + } + self.garage.key_table.insert(&key).await?; + self.key_info_result(key).await + } + async fn handle_import_key(&self, query: &KeyImportOpt) -> Result { let prev_key = self.garage.key_table.get(&EmptyKey, &query.key_id).await?; if prev_key.is_some() { diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index bd7abc8e..a544d6a1 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -274,6 +274,14 @@ pub enum KeyOperation { #[structopt(name = "delete")] Delete(KeyDeleteOpt), + /// Set permission flags for key + #[structopt(name = "allow")] + Allow(KeyPermOpt), + + /// Unset permission flags for key + #[structopt(name = "deny")] + Deny(KeyPermOpt), + /// Import key #[structopt(name = "import")] Import(KeyImportOpt), @@ -311,6 +319,16 @@ pub struct KeyDeleteOpt { pub yes: bool, } +#[derive(Serialize, Deserialize, StructOpt, Debug)] +pub struct KeyPermOpt { + /// ID or name of the key + pub key_pattern: String, + + /// Flag that allows key to create buckets using S3's CreateBucket call + #[structopt(long = "create-bucket")] + pub create_bucket: bool, +} + #[derive(Serialize, Deserialize, StructOpt, Debug)] pub struct KeyImportOpt { /// Access key ID diff --git a/src/garage/cli/util.rs b/src/garage/cli/util.rs index 365831c4..61401e4c 100644 --- a/src/garage/cli/util.rs +++ b/src/garage/cli/util.rs @@ -18,15 +18,15 @@ pub fn print_bucket_list(bl: Vec) { .filter(|(_, _, active)| *active) .map(|(name, _, _)| name.to_string()) .collect::>(); - let local_aliases_n = match bucket + let local_aliases_n = match &bucket .local_aliases() .iter() .filter(|(_, _, active)| *active) - .count() + .collect::>()[..] { - 0 => "".into(), - 1 => "1 local alias".into(), - n => format!("{} local aliases", n), + [] => "".into(), + [((k, n), _, _)] => format!("{}:{}", k, n), + s => format!("[{} local aliases]", s.len()), }; table.push(format!( "\t{}\t{}\t{}", @@ -88,6 +88,9 @@ pub fn print_key_info(key: &Key, relevant_buckets: &HashMap) { println!("\nAuthorized buckets:"); let mut table = vec![]; for (bucket_id, perm) in p.authorized_buckets.items().iter() { + if !perm.is_any() { + continue; + } let rflag = if perm.allow_read { "R" } else { " " }; let wflag = if perm.allow_write { "W" } else { " " }; let oflag = if perm.allow_owner { "O" } else { " " }; diff --git a/src/model/helper/bucket.rs b/src/model/helper/bucket.rs index 6f171c8b..92b9f4cd 100644 --- a/src/model/helper/bucket.rs +++ b/src/model/helper/bucket.rs @@ -433,13 +433,11 @@ impl<'a> BucketHelper<'a> { let mut bucket = self.get_internal_bucket(bucket_id).await?; let mut key = self.get_internal_key(key_id).await?; - let allow_any = perm.allow_read || perm.allow_write || perm.allow_owner; - if let Some(bstate) = bucket.state.as_option() { if let Some(kp) = bstate.authorized_keys.get(key_id) { perm.timestamp = increment_logical_clock_2(perm.timestamp, kp.timestamp); } - } else if allow_any { + } else if perm.is_any() { return Err(Error::BadRequest( "Trying to give permissions on a deleted bucket".into(), )); @@ -449,7 +447,7 @@ impl<'a> BucketHelper<'a> { if let Some(bp) = kstate.authorized_buckets.get(&bucket_id) { perm.timestamp = increment_logical_clock_2(perm.timestamp, bp.timestamp); } - } else if allow_any { + } else if perm.is_any() { return Err(Error::BadRequest( "Trying to give permissions to a deleted key".into(), )); diff --git a/src/model/permission.rs b/src/model/permission.rs index 67527ed0..1eaddf00 100644 --- a/src/model/permission.rs +++ b/src/model/permission.rs @@ -27,6 +27,17 @@ impl BucketKeyPerm { allow_write: false, allow_owner: false, }; + + pub const ALL_PERMISSIONS: Self = Self { + timestamp: 0, + allow_read: true, + allow_write: true, + allow_owner: true, + }; + + pub fn is_any(&self) -> bool { + self.allow_read || self.allow_write || self.allow_owner + } } impl Crdt for BucketKeyPerm {