aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2021-12-15 18:36:15 +0100
committerAlex Auvolat <alex@adnab.me>2022-01-04 12:45:51 +0100
commit53f71b3a57b3c1828292e26b7865d31e9bec44d6 (patch)
treeb874fa38a3680b7ba153d34a711f4ebff6884c00
parent5b1117e582db16cc5aa50840a685875cbd5501f4 (diff)
downloadgarage-53f71b3a57b3c1828292e26b7865d31e9bec44d6.tar.gz
garage-53f71b3a57b3c1828292e26b7865d31e9bec44d6.zip
Implement bucket alias and bucket unalias
-rw-r--r--src/garage/admin.rs187
-rw-r--r--src/garage/cli/cmd.rs5
-rw-r--r--src/garage/cli/structs.rs31
-rw-r--r--src/garage/cli/util.rs11
-rw-r--r--src/model/bucket_helper.rs28
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<AdminRpc, Error> {
+ 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<AdminRpc, Error> {
+ 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<AdminRpc, Error> {
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),
@@ -194,6 +202,29 @@ pub struct DeleteBucketOpt {
}
#[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<String>,
+}
+
+#[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<String>,
+}
+
+#[derive(Serialize, Deserialize, StructOpt, Debug)]
pub struct PermBucketOpt {
/// Access key name or ID
#[structopt(long = "key")]
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<Option<Uuid>, 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)]