Implement bucket alias and bucket unalias

This commit is contained in:
Alex Auvolat 2021-12-15 18:36:15 +01:00
parent 5b1117e582
commit 53f71b3a57
No known key found for this signature in database
GPG key ID: EDABF9711E244EB1
5 changed files with 252 additions and 10 deletions

View file

@ -77,6 +77,8 @@ impl AdminRpcHandler {
} }
BucketOperation::Create(query) => self.handle_create_bucket(&query.name).await, BucketOperation::Create(query) => self.handle_create_bucket(&query.name).await,
BucketOperation::Delete(query) => self.handle_delete_bucket(query).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::Allow(query) => self.handle_bucket_allow(query).await,
BucketOperation::Deny(query) => self.handle_bucket_deny(query).await, BucketOperation::Deny(query) => self.handle_bucket_deny(query).await,
BucketOperation::Website(query) => self.handle_bucket_website(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))) 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> { async fn handle_bucket_allow(&self, query: &PermBucketOpt) -> Result<AdminRpc, Error> {
let bucket_id = self let bucket_id = self
.garage .garage

View file

@ -161,12 +161,15 @@ pub async fn cmd_admin(
} }
AdminRpc::BucketList(bl) => { AdminRpc::BucketList(bl) => {
println!("List of buckets:"); println!("List of buckets:");
let mut table = vec![];
for alias in bl { for alias in bl {
if let Some(p) = alias.state.get().as_option() { if let Some(p) = alias.state.get().as_option() {
let wflag = if p.website_access { "W" } else { " " }; 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) => { AdminRpc::BucketInfo(bucket) => {
print_bucket_info(&bucket); print_bucket_info(&bucket);

View file

@ -150,6 +150,14 @@ pub enum BucketOperation {
#[structopt(name = "delete")] #[structopt(name = "delete")]
Delete(DeleteBucketOpt), 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 /// Allow key to read or write to bucket
#[structopt(name = "allow")] #[structopt(name = "allow")]
Allow(PermBucketOpt), Allow(PermBucketOpt),
@ -193,6 +201,29 @@ pub struct DeleteBucketOpt {
pub yes: bool, 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<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)] #[derive(Serialize, Deserialize, StructOpt, Debug)]
pub struct PermBucketOpt { pub struct PermBucketOpt {
/// Access key name or ID /// Access key name or ID

View file

@ -12,17 +12,22 @@ pub fn print_key_info(key: &Key) {
match &key.state { match &key.state {
Deletable::Present(p) => { Deletable::Present(p) => {
println!("\nKey-specific bucket aliases:"); println!("\nKey-specific bucket aliases:");
let mut table = vec![];
for (alias_name, _, alias) in p.local_aliases.items().iter() { for (alias_name, _, alias) in p.local_aliases.items().iter() {
if let Some(bucket_id) = alias.as_option() { 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:"); println!("\nAuthorized buckets:");
let mut table = vec![];
for (b, perm) in p.authorized_buckets.items().iter() { for (b, perm) in p.authorized_buckets.items().iter() {
let rflag = if perm.allow_read { "R" } else { " " }; let rflag = if perm.allow_read { "R" } else { " " };
let wflag = if perm.allow_write { "W" } 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 => { Deletable::Deleted => {
println!("\nKey is deleted."); println!("\nKey is deleted.");
@ -41,12 +46,14 @@ pub fn print_bucket_info(bucket: &Bucket) {
println!("- {}", alias); println!("- {}", alias);
} }
} }
println!("\nKey-specific aliases:"); println!("\nKey-specific aliases:");
for ((key_id, alias), _, active) in p.local_aliases.items().iter() { for ((key_id, alias), _, active) in p.local_aliases.items().iter() {
if *active { if *active {
println!("- {} {}", key_id, alias); println!("- {} {}", key_id, alias);
} }
} }
println!("\nAuthorized keys:"); println!("\nAuthorized keys:");
for (k, perm) in p.authorized_keys.items().iter() { for (k, perm) in p.authorized_keys.items().iter() {
let rflag = if perm.allow_read { "R" } else { " " }; let rflag = if perm.allow_read { "R" } else { " " };

View file

@ -14,6 +14,19 @@ impl<'a> BucketHelper<'a> {
&self, &self,
bucket_name: &String, bucket_name: &String,
) -> Result<Option<Uuid>, Error> { ) -> Result<Option<Uuid>, Error> {
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 Ok(self
.0 .0
.bucket_alias_table .bucket_alias_table
@ -22,6 +35,7 @@ impl<'a> BucketHelper<'a> {
.map(|x| x.state.get().as_option().map(|x| x.bucket_id)) .map(|x| x.state.get().as_option().map(|x| x.bucket_id))
.flatten()) .flatten())
} }
}
#[allow(clippy::ptr_arg)] #[allow(clippy::ptr_arg)]
pub async fn get_existing_bucket(&self, bucket_id: Uuid) -> Result<Bucket, Error> { pub async fn get_existing_bucket(&self, bucket_id: Uuid) -> Result<Bucket, Error> {