aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2022-01-05 15:56:48 +0100
committerAlex Auvolat <alex@adnab.me>2022-01-05 15:56:48 +0100
commit8395030e4891ae48ca30428318e8d435c157f74f (patch)
treeecbaac13a70971eb6b2d11df65bdb5f28f441484
parent9431090b1eb9006b12395fb22700b0def7fd1f59 (diff)
downloadgarage-8395030e4891ae48ca30428318e8d435c157f74f.tar.gz
garage-8395030e4891ae48ca30428318e8d435c157f74f.zip
Implement CreateBucket
-rw-r--r--src/api/api_server.rs21
-rw-r--r--src/api/s3_bucket.rs146
-rw-r--r--src/garage/admin.rs10
-rw-r--r--src/model/helper/bucket.rs6
-rw-r--r--src/model/permission.rs11
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<Garage>, req: Request<Body>) -> Result<Respon
let endpoint = Endpoint::from_request(&req, bucket.map(ToOwned::to_owned))?;
+ // Special code path for CreateBucket API endpoint
+ 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,
Some(bucket) => bucket.to_string(),
@@ -188,19 +193,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
)
.await
}
- Endpoint::CreateBucket { bucket } => {
- debug!(
- "Body: {}",
- std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?)
- .unwrap_or("<invalid utf8>")
- );
- 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<Garage>) -> Result<Response<Body>, Error> {
let loc = s3_xml::LocationConstraint {
@@ -50,7 +55,7 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result<Respo
.authorized_buckets
.items()
.iter()
- .filter(|(_, perms)| perms.allow_read || perms.allow_write || perms.allow_owner)
+ .filter(|(_, perms)| perms.is_any())
.map(|(id, _)| *id)
.collect::<Vec<_>>();
@@ -105,3 +110,140 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result<Respo
.header("Content-Type", "application/xml")
.body(Body::from(xml))?)
}
+
+pub async fn handle_create_bucket(
+ garage: &Garage,
+ req: Request<Body>,
+ content_sha256: Option<Hash>,
+ api_key: Key,
+ bucket_name: String,
+) -> Result<Response<Body>, 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<Option<String>> {
+ // 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#"
+ <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+ <LocationConstraint>Europe</LocationConstraint>
+ </CreateBucketConfiguration >
+ "#
+ ),
+ Some("Europe")
+ );
+ assert_eq!(
+ parse_create_bucket_xml(
+ br#"
+ <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+ </CreateBucketConfiguration >
+ "#
+ ),
+ 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 {