forked from Deuxfleurs/garage
Implement CreateBucket
This commit is contained in:
parent
9431090b1e
commit
8395030e48
5 changed files with 166 additions and 28 deletions
|
@ -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))?;
|
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() {
|
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(),
|
||||||
|
@ -188,19 +193,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Endpoint::CreateBucket { bucket } => {
|
Endpoint::CreateBucket { .. } => unreachable!(),
|
||||||
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::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();
|
||||||
|
@ -303,7 +296,7 @@ async fn resolve_bucket(
|
||||||
let api_key_params = api_key
|
let api_key_params = api_key
|
||||||
.state
|
.state
|
||||||
.as_option()
|
.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) {
|
if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) {
|
||||||
Ok(*bucket_id)
|
Ok(*bucket_id)
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
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::garage::Garage;
|
||||||
use garage_model::key_table::Key;
|
use garage_model::key_table::Key;
|
||||||
|
use garage_model::permission::BucketKeyPerm;
|
||||||
use garage_table::util::EmptyKey;
|
use garage_table::util::EmptyKey;
|
||||||
use garage_util::crdt::*;
|
use garage_util::crdt::*;
|
||||||
|
use garage_util::data::*;
|
||||||
use garage_util::time::*;
|
use garage_util::time::*;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::s3_xml;
|
use crate::s3_xml;
|
||||||
|
use crate::signature::verify_signed_content;
|
||||||
|
|
||||||
pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>, Error> {
|
pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
let loc = s3_xml::LocationConstraint {
|
let loc = s3_xml::LocationConstraint {
|
||||||
|
@ -50,7 +55,7 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result<Respo
|
||||||
.authorized_buckets
|
.authorized_buckets
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, perms)| perms.allow_read || perms.allow_write || perms.allow_owner)
|
.filter(|(_, perms)| perms.is_any())
|
||||||
.map(|(id, _)| *id)
|
.map(|(id, _)| *id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -105,3 +110,140 @@ pub async fn handle_list_buckets(garage: &Garage, api_key: &Key) -> Result<Respo
|
||||||
.header("Content-Type", "application/xml")
|
.header("Content-Type", "application/xml")
|
||||||
.body(Body::from(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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -531,10 +531,7 @@ impl AdminRpcHandler {
|
||||||
.bucket_helper()
|
.bucket_helper()
|
||||||
.get_existing_matching_key(&query.key_pattern)
|
.get_existing_matching_key(&query.key_pattern)
|
||||||
.await?;
|
.await?;
|
||||||
key.params_mut()
|
key.params_mut().unwrap().allow_create_bucket.update(true);
|
||||||
.unwrap()
|
|
||||||
.allow_create_bucket
|
|
||||||
.update(true);
|
|
||||||
self.garage.key_table.insert(&key).await?;
|
self.garage.key_table.insert(&key).await?;
|
||||||
self.key_info_result(key).await
|
self.key_info_result(key).await
|
||||||
}
|
}
|
||||||
|
@ -545,10 +542,7 @@ impl AdminRpcHandler {
|
||||||
.bucket_helper()
|
.bucket_helper()
|
||||||
.get_existing_matching_key(&query.key_pattern)
|
.get_existing_matching_key(&query.key_pattern)
|
||||||
.await?;
|
.await?;
|
||||||
key.params_mut()
|
key.params_mut().unwrap().allow_create_bucket.update(false);
|
||||||
.unwrap()
|
|
||||||
.allow_create_bucket
|
|
||||||
.update(false);
|
|
||||||
self.garage.key_table.insert(&key).await?;
|
self.garage.key_table.insert(&key).await?;
|
||||||
self.key_info_result(key).await
|
self.key_info_result(key).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -433,13 +433,11 @@ impl<'a> BucketHelper<'a> {
|
||||||
let mut bucket = self.get_internal_bucket(bucket_id).await?;
|
let mut bucket = self.get_internal_bucket(bucket_id).await?;
|
||||||
let mut key = self.get_internal_key(key_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(bstate) = bucket.state.as_option() {
|
||||||
if let Some(kp) = bstate.authorized_keys.get(key_id) {
|
if let Some(kp) = bstate.authorized_keys.get(key_id) {
|
||||||
perm.timestamp = increment_logical_clock_2(perm.timestamp, kp.timestamp);
|
perm.timestamp = increment_logical_clock_2(perm.timestamp, kp.timestamp);
|
||||||
}
|
}
|
||||||
} else if allow_any {
|
} else if perm.is_any() {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
"Trying to give permissions on a deleted bucket".into(),
|
"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) {
|
if let Some(bp) = kstate.authorized_buckets.get(&bucket_id) {
|
||||||
perm.timestamp = increment_logical_clock_2(perm.timestamp, bp.timestamp);
|
perm.timestamp = increment_logical_clock_2(perm.timestamp, bp.timestamp);
|
||||||
}
|
}
|
||||||
} else if allow_any {
|
} else if perm.is_any() {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
"Trying to give permissions to a deleted key".into(),
|
"Trying to give permissions to a deleted key".into(),
|
||||||
));
|
));
|
||||||
|
|
|
@ -27,6 +27,17 @@ impl BucketKeyPerm {
|
||||||
allow_write: false,
|
allow_write: false,
|
||||||
allow_owner: 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 {
|
impl Crdt for BucketKeyPerm {
|
||||||
|
|
Loading…
Reference in a new issue