From 53f71b3a57b3c1828292e26b7865d31e9bec44d6 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 15 Dec 2021 18:36:15 +0100 Subject: [PATCH] Implement bucket alias and bucket unalias --- src/garage/admin.rs | 187 +++++++++++++++++++++++++++++++++++++ src/garage/cli/cmd.rs | 5 +- src/garage/cli/structs.rs | 31 ++++++ src/garage/cli/util.rs | 11 ++- src/model/bucket_helper.rs | 28 ++++-- 5 files changed, 252 insertions(+), 10 deletions(-) diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 6db8bfbe..756f6007 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -77,6 +77,8 @@ impl AdminRpcHandler { } BucketOperation::Create(query) => self.handle_create_bucket(&query.name).await, BucketOperation::Delete(query) => self.handle_delete_bucket(query).await, + BucketOperation::Alias(query) => self.handle_alias_bucket(query).await, + BucketOperation::Unalias(query) => self.handle_unalias_bucket(query).await, BucketOperation::Allow(query) => self.handle_bucket_allow(query).await, BucketOperation::Deny(query) => self.handle_bucket_deny(query).await, BucketOperation::Website(query) => self.handle_bucket_website(query).await, @@ -193,6 +195,191 @@ impl AdminRpcHandler { Ok(AdminRpc::Ok(format!("Bucket {} was deleted.", query.name))) } + async fn handle_alias_bucket(&self, query: &AliasBucketOpt) -> Result { + let bucket_id = self + .garage + .bucket_helper() + .resolve_global_bucket_name(&query.existing_bucket) + .await? + .ok_or_message("Bucket not found")?; + let mut bucket = self + .garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + + if let Some(key_local) = &query.local { + let mut key = self.get_existing_key(key_local).await?; + let mut key_param = key.state.as_option_mut().unwrap(); + + if let Some(Deletable::Present(existing_alias)) = + key_param.local_aliases.get(&query.new_name) + { + if *existing_alias == bucket_id { + return Ok(AdminRpc::Ok(format!( + "Alias {} already points to bucket {:?} in namespace of key {}", + query.new_name, bucket_id, key.key_id + ))); + } else { + return Err(Error::Message(format!("Alias {} already exists and points to different bucket: {:?} in namespace of key {}", query.new_name, existing_alias, key.key_id))); + } + } + + key_param.local_aliases = key_param + .local_aliases + .update_mutator(query.new_name.clone(), Deletable::present(bucket_id)); + self.garage.key_table.insert(&key).await?; + + let mut bucket_p = bucket.state.as_option_mut().unwrap(); + bucket_p.local_aliases = bucket_p + .local_aliases + .update_mutator((key.key_id.clone(), query.new_name.clone()), true); + self.garage.bucket_table.insert(&bucket).await?; + + Ok(AdminRpc::Ok(format!( + "Alias {} created to bucket {:?} in namespace of key {}", + query.new_name, bucket_id, key.key_id + ))) + } else { + let mut alias = self + .garage + .bucket_alias_table + .get(&EmptyKey, &query.new_name) + .await? + .unwrap_or(BucketAlias { + name: query.new_name.clone(), + state: Lww::new(Deletable::delete()), + }); + + if let Some(existing_alias) = alias.state.get().as_option() { + if existing_alias.bucket_id == bucket_id { + return Ok(AdminRpc::Ok(format!( + "Alias {} already points to bucket {:?}", + query.new_name, bucket_id + ))); + } else { + return Err(Error::Message(format!( + "Alias {} already exists and points to different bucket: {:?}", + query.new_name, existing_alias.bucket_id + ))); + } + } + + // Checks ok, add alias + alias.state.update(Deletable::present(AliasParams { + bucket_id, + website_access: false, + })); + self.garage.bucket_alias_table.insert(&alias).await?; + + let mut bucket_p = bucket.state.as_option_mut().unwrap(); + bucket_p.aliases = bucket_p + .aliases + .update_mutator(query.new_name.clone(), true); + self.garage.bucket_table.insert(&bucket).await?; + + Ok(AdminRpc::Ok(format!( + "Alias {} created to bucket {:?}", + query.new_name, bucket_id + ))) + } + } + + async fn handle_unalias_bucket(&self, query: &UnaliasBucketOpt) -> Result { + if let Some(key_local) = &query.local { + let mut key = self.get_existing_key(key_local).await?; + + let bucket_id = key + .state + .as_option() + .unwrap() + .local_aliases + .get(&query.name) + .map(|a| a.into_option()) + .flatten() + .ok_or_message("Bucket not found")?; + let mut bucket = self + .garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + let mut bucket_state = bucket.state.as_option_mut().unwrap(); + + let has_other_aliases = bucket_state + .aliases + .items() + .iter() + .any(|(_, _, active)| *active) + || bucket_state + .local_aliases + .items() + .iter() + .any(|((k, n), _, active)| *k == key.key_id && *n == query.name && *active); + if !has_other_aliases { + return Err(Error::Message(format!("Bucket {} doesn't have other aliases, please delete it instead of just unaliasing.", query.name))); + } + + let mut key_param = key.state.as_option_mut().unwrap(); + key_param.local_aliases = key_param + .local_aliases + .update_mutator(query.name.clone(), Deletable::delete()); + self.garage.key_table.insert(&key).await?; + + bucket_state.local_aliases = bucket_state + .local_aliases + .update_mutator((key.key_id.clone(), query.name.clone()), false); + self.garage.bucket_table.insert(&bucket).await?; + + Ok(AdminRpc::Ok(format!( + "Bucket alias {} deleted from namespace of key {}", + query.name, key.key_id + ))) + } else { + let bucket_id = self + .garage + .bucket_helper() + .resolve_global_bucket_name(&query.name) + .await? + .ok_or_message("Bucket not found")?; + let mut bucket = self + .garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + let mut bucket_state = bucket.state.as_option_mut().unwrap(); + + let has_other_aliases = bucket_state + .aliases + .items() + .iter() + .any(|(name, _, active)| *name != query.name && *active) + || bucket_state + .local_aliases + .items() + .iter() + .any(|(_, _, active)| *active); + if !has_other_aliases { + return Err(Error::Message(format!("Bucket {} doesn't have other aliases, please delete it instead of just unaliasing.", query.name))); + } + + let mut alias = self + .garage + .bucket_alias_table + .get(&EmptyKey, &query.name) + .await? + .ok_or_message("Internal error: alias not found")?; + alias.state.update(Deletable::delete()); + self.garage.bucket_alias_table.insert(&alias).await?; + + bucket_state.aliases = bucket_state + .aliases + .update_mutator(query.name.clone(), false); + self.garage.bucket_table.insert(&bucket).await?; + + Ok(AdminRpc::Ok(format!("Bucket alias {} deleted", query.name))) + } + } + async fn handle_bucket_allow(&self, query: &PermBucketOpt) -> Result { let bucket_id = self .garage diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 3cdf4d26..015eeec9 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -161,12 +161,15 @@ pub async fn cmd_admin( } AdminRpc::BucketList(bl) => { println!("List of buckets:"); + let mut table = vec![]; for alias in bl { if let Some(p) = alias.state.get().as_option() { let wflag = if p.website_access { "W" } else { " " }; - println!("- {} {} {:?}", wflag, alias.name, p.bucket_id); + table.push(format!("{}\t{}\t{:?}", wflag, alias.name, p.bucket_id)); } } + format_table(table); + println!("Buckets that don't have a global alias (i.e. that only exist in the namespace of an access key) are not shown."); } AdminRpc::BucketInfo(bucket) => { print_bucket_info(&bucket); diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index b2b5375d..590be1c0 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -150,6 +150,14 @@ pub enum BucketOperation { #[structopt(name = "delete")] Delete(DeleteBucketOpt), + /// Alias bucket under new name + #[structopt(name = "alias")] + Alias(AliasBucketOpt), + + /// Remove bucket alias + #[structopt(name = "unalias")] + Unalias(UnaliasBucketOpt), + /// Allow key to read or write to bucket #[structopt(name = "allow")] Allow(PermBucketOpt), @@ -193,6 +201,29 @@ pub struct DeleteBucketOpt { pub yes: bool, } +#[derive(Serialize, Deserialize, StructOpt, Debug)] +pub struct AliasBucketOpt { + /// Existing bucket name (its alias in global namespace or its full hex uuid) + pub existing_bucket: String, + + /// New bucket name + pub new_name: String, + + /// Make this alias local to the specified access key + #[structopt(long = "local")] + pub local: Option, +} + +#[derive(Serialize, Deserialize, StructOpt, Debug)] +pub struct UnaliasBucketOpt { + /// Bucket name + pub name: String, + + /// Unalias in bucket namespace local to this access key + #[structopt(long = "local")] + pub local: Option, +} + #[derive(Serialize, Deserialize, StructOpt, Debug)] pub struct PermBucketOpt { /// Access key name or ID diff --git a/src/garage/cli/util.rs b/src/garage/cli/util.rs index be34183e..ba88502d 100644 --- a/src/garage/cli/util.rs +++ b/src/garage/cli/util.rs @@ -12,17 +12,22 @@ pub fn print_key_info(key: &Key) { match &key.state { Deletable::Present(p) => { println!("\nKey-specific bucket aliases:"); + let mut table = vec![]; for (alias_name, _, alias) in p.local_aliases.items().iter() { if let Some(bucket_id) = alias.as_option() { - println!("- {} {:?}", alias_name, bucket_id); + table.push(format!("\t{}\t{}", alias_name, hex::encode(bucket_id))); } } + format_table(table); + println!("\nAuthorized buckets:"); + let mut table = vec![]; for (b, perm) in p.authorized_buckets.items().iter() { let rflag = if perm.allow_read { "R" } else { " " }; let wflag = if perm.allow_write { "W" } else { " " }; - println!("- {}{} {:?}", rflag, wflag, b); + table.push(format!("\t{}{}\t{:?}", rflag, wflag, b)); } + format_table(table); } Deletable::Deleted => { println!("\nKey is deleted."); @@ -41,12 +46,14 @@ pub fn print_bucket_info(bucket: &Bucket) { println!("- {}", alias); } } + println!("\nKey-specific aliases:"); for ((key_id, alias), _, active) in p.local_aliases.items().iter() { if *active { println!("- {} {}", key_id, alias); } } + println!("\nAuthorized keys:"); for (k, perm) in p.authorized_keys.items().iter() { let rflag = if perm.allow_read { "R" } else { " " }; diff --git a/src/model/bucket_helper.rs b/src/model/bucket_helper.rs index e0720b4e..c1280afa 100644 --- a/src/model/bucket_helper.rs +++ b/src/model/bucket_helper.rs @@ -14,13 +14,27 @@ impl<'a> BucketHelper<'a> { &self, bucket_name: &String, ) -> Result, Error> { - Ok(self - .0 - .bucket_alias_table - .get(&EmptyKey, bucket_name) - .await? - .map(|x| x.state.get().as_option().map(|x| x.bucket_id)) - .flatten()) + let hexbucket = hex::decode(bucket_name.as_str()) + .ok() + .map(|by| Uuid::try_from(&by)) + .flatten(); + if let Some(bucket_id) = hexbucket { + Ok(self + .0 + .bucket_table + .get(&bucket_id, &EmptyKey) + .await? + .filter(|x| !x.state.is_deleted()) + .map(|_| bucket_id)) + } else { + Ok(self + .0 + .bucket_alias_table + .get(&EmptyKey, bucket_name) + .await? + .map(|x| x.state.get().as_option().map(|x| x.bucket_id)) + .flatten()) + } } #[allow(clippy::ptr_arg)]