From 8395030e4891ae48ca30428318e8d435c157f74f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 5 Jan 2022 15:56:48 +0100 Subject: [PATCH] Implement CreateBucket --- src/api/api_server.rs | 21 ++---- src/api/s3_bucket.rs | 146 ++++++++++++++++++++++++++++++++++++- src/garage/admin.rs | 10 +-- src/model/helper/bucket.rs | 6 +- src/model/permission.rs | 11 +++ 5 files changed, 166 insertions(+), 28 deletions(-) diff --git a/src/api/api_server.rs b/src/api/api_server.rs index f5ebed37..db5da8cc 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -108,6 +108,11 @@ async fn handler_inner(garage: Arc, req: Request) -> Result return handle_request_without_bucket(garage, req, api_key, endpoint).await, Some(bucket) => bucket.to_string(), @@ -188,19 +193,7 @@ 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(); @@ -303,7 +296,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) diff --git a/src/api/s3_bucket.rs b/src/api/s3_bucket.rs index 27208ffa..23f43317 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}; +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_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,140 @@ 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!( + "Buckets must be created in region {}", + 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::Forbidden(format!( + "Key {} does not have write or owner permissions on bucket {}", + api_key.key_id, bucket_name + ))); + } + } 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()) +} + +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 root = xml.root(); + let cbc = root.first_child()?; + if !cbc.has_tag_name("CreateBucketConfiguration") { + return None; + } + + let mut ret = None; + for item in cbc.children() { + if item.has_tag_name("LocationConstraint") { + if ret != None { + return None; + } + ret = Some(item.text()?.to_string()); + } else { + return None; + } + } + + Some(ret) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_bucket() -> Result<(), ()> { + assert_eq!( + parse_create_bucket_xml( + br#" + + Europe + + "# + ), + Some("Europe") + ); + assert_eq!( + parse_create_bucket_xml( + br#" + + + "# + ), + None + ); + } +} diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 509ecaf9..9bfca94e 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -531,10 +531,7 @@ impl AdminRpcHandler { .bucket_helper() .get_existing_matching_key(&query.key_pattern) .await?; - key.params_mut() - .unwrap() - .allow_create_bucket - .update(true); + key.params_mut().unwrap().allow_create_bucket.update(true); self.garage.key_table.insert(&key).await?; self.key_info_result(key).await } @@ -545,10 +542,7 @@ impl AdminRpcHandler { .bucket_helper() .get_existing_matching_key(&query.key_pattern) .await?; - key.params_mut() - .unwrap() - .allow_create_bucket - .update(false); + key.params_mut().unwrap().allow_create_bucket.update(false); self.garage.key_table.insert(&key).await?; self.key_info_result(key).await } 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 {