more complete admin API #298

Merged
lx merged 48 commits from admin-api into main 2022-05-24 10:16:40 +00:00
7 changed files with 227 additions and 105 deletions
Showing only changes of commit 393b76ecba - Show all commits

View file

@ -136,10 +136,15 @@ impl ApiHandler for AdminApiServer {
Endpoint::GetKeyInfo { id, search } => { Endpoint::GetKeyInfo { id, search } => {
handle_get_key_info(&self.garage, id, search).await handle_get_key_info(&self.garage, id, search).await
} }
Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await,
Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await,
/*
_ => Err(Error::NotImplemented(format!( _ => Err(Error::NotImplemented(format!(
"Admin endpoint {} not implemented yet", "Admin endpoint {} not implemented yet",
endpoint.name() endpoint.name()
))), ))),
*/
} }
} }
} }

View file

@ -1,16 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use garage_util::crdt::*;
use garage_util::data::*;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use garage_rpc::layout::*;
use garage_table::*; use garage_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
@ -62,7 +57,7 @@ pub async fn handle_get_key_info(
.ok_or(Error::NoSuchKey)? .ok_or(Error::NoSuchKey)?
} else if let Some(search) = search { } else if let Some(search) = search {
garage garage
.bucket_helper() .key_helper()
.get_existing_matching_key(&search) .get_existing_matching_key(&search)
.await .await
.map_err(|_| Error::NoSuchKey)? .map_err(|_| Error::NoSuchKey)?
@ -70,6 +65,84 @@ pub async fn handle_get_key_info(
unreachable!(); unreachable!();
}; };
key_info_results(garage, key).await
}
pub async fn handle_create_key(
garage: &Arc<Garage>,
req: Request<Body>,
) -> Result<Response<Body>, Error> {
let req = parse_json_body::<CreateKeyRequest>(req).await?;
let key = Key::new(&req.name);
garage.key_table.insert(&key).await?;
key_info_results(garage, key).await
}
#[derive(Deserialize)]
struct CreateKeyRequest {
name: String,
}
pub async fn handle_update_key(
garage: &Arc<Garage>,
id: String,
req: Request<Body>,
) -> Result<Response<Body>, Error> {
let req = parse_json_body::<UpdateKeyRequest>(req).await?;
let mut key = garage
.key_table
.get(&EmptyKey, &id)
.await?
.ok_or(Error::NoSuchKey)?;
let key_state = key.state.as_option_mut().ok_or(Error::NoSuchKey)?;
if let Some(new_name) = req.name {
key_state.name.update(new_name);
}
if let Some(allow) = req.allow {
if allow.create_bucket {
key_state.allow_create_bucket.update(true);
}
}
if let Some(deny) = req.deny {
if deny.create_bucket {
key_state.allow_create_bucket.update(false);
}
}
garage.key_table.insert(&key).await?;
key_info_results(garage, key).await
}
#[derive(Deserialize)]
struct UpdateKeyRequest {
name: Option<String>,
allow: Option<KeyPerm>,
deny: Option<KeyPerm>,
}
pub async fn handle_delete_key(garage: &Arc<Garage>, id: String) -> Result<Response<Body>, Error> {
let mut key = garage
.key_table
.get(&EmptyKey, &id)
.await?
.ok_or(Error::NoSuchKey)?;
key.state.as_option().ok_or(Error::NoSuchKey)?;
garage.key_helper().delete_key(&mut key).await?;
Ok(Response::builder()
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Body>, Error> {
let mut relevant_buckets = HashMap::new(); let mut relevant_buckets = HashMap::new();
let key_state = key.state.as_option().unwrap(); let key_state = key.state.as_option().unwrap();
@ -99,7 +172,7 @@ pub async fn handle_get_key_info(
name: key_state.name.get().clone(), name: key_state.name.get().clone(),
access_key_id: key.key_id.clone(), access_key_id: key.key_id.clone(),
secret_access_key: key_state.secret_key.clone(), secret_access_key: key_state.secret_key.clone(),
permissions: KeyPermResult { permissions: KeyPerm {
create_bucket: *key_state.allow_create_bucket.get(), create_bucket: *key_state.allow_create_bucket.get(),
}, },
buckets: relevant_buckets buckets: relevant_buckets
@ -153,13 +226,13 @@ struct GetKeyInfoResult {
access_key_id: String, access_key_id: String,
#[serde(rename = "secretAccessKey")] #[serde(rename = "secretAccessKey")]
secret_access_key: String, secret_access_key: String,
permissions: KeyPermResult, permissions: KeyPerm,
buckets: Vec<KeyInfoBucketResult>, buckets: Vec<KeyInfoBucketResult>,
} }
#[derive(Serialize)] #[derive(Serialize, Deserialize)]
struct KeyPermResult { struct KeyPerm {
#[serde(rename = "createBucket")] #[serde(rename = "createBucket", default)]
create_bucket: bool, create_bucket: bool,
} }

View file

@ -261,6 +261,7 @@ impl AdminRpcHandler {
async fn handle_alias_bucket(&self, query: &AliasBucketOpt) -> Result<AdminRpc, Error> { async fn handle_alias_bucket(&self, query: &AliasBucketOpt) -> Result<AdminRpc, Error> {
let helper = self.garage.bucket_helper(); let helper = self.garage.bucket_helper();
let key_helper = self.garage.key_helper();
let bucket_id = helper let bucket_id = helper
.resolve_global_bucket_name(&query.existing_bucket) .resolve_global_bucket_name(&query.existing_bucket)
@ -268,7 +269,7 @@ impl AdminRpcHandler {
.ok_or_bad_request("Bucket not found")?; .ok_or_bad_request("Bucket not found")?;
if let Some(key_pattern) = &query.local { if let Some(key_pattern) = &query.local {
let key = helper.get_existing_matching_key(key_pattern).await?; let key = key_helper.get_existing_matching_key(key_pattern).await?;
helper helper
.set_local_bucket_alias(bucket_id, &key.key_id, &query.new_name) .set_local_bucket_alias(bucket_id, &key.key_id, &query.new_name)
@ -290,9 +291,10 @@ impl AdminRpcHandler {
async fn handle_unalias_bucket(&self, query: &UnaliasBucketOpt) -> Result<AdminRpc, Error> { async fn handle_unalias_bucket(&self, query: &UnaliasBucketOpt) -> Result<AdminRpc, Error> {
let helper = self.garage.bucket_helper(); let helper = self.garage.bucket_helper();
let key_helper = self.garage.key_helper();
if let Some(key_pattern) = &query.local { if let Some(key_pattern) = &query.local {
let key = helper.get_existing_matching_key(key_pattern).await?; let key = key_helper.get_existing_matching_key(key_pattern).await?;
let bucket_id = key let bucket_id = key
.state .state
@ -331,12 +333,15 @@ impl AdminRpcHandler {
async fn handle_bucket_allow(&self, query: &PermBucketOpt) -> Result<AdminRpc, Error> { async fn handle_bucket_allow(&self, query: &PermBucketOpt) -> Result<AdminRpc, Error> {
let helper = self.garage.bucket_helper(); let helper = self.garage.bucket_helper();
let key_helper = self.garage.key_helper();
let bucket_id = helper let bucket_id = helper
.resolve_global_bucket_name(&query.bucket) .resolve_global_bucket_name(&query.bucket)
.await? .await?
.ok_or_bad_request("Bucket not found")?; .ok_or_bad_request("Bucket not found")?;
let key = helper.get_existing_matching_key(&query.key_pattern).await?; let key = key_helper
.get_existing_matching_key(&query.key_pattern)
.await?;
let allow_read = query.read || key.allow_read(&bucket_id); let allow_read = query.read || key.allow_read(&bucket_id);
let allow_write = query.write || key.allow_write(&bucket_id); let allow_write = query.write || key.allow_write(&bucket_id);
@ -363,12 +368,15 @@ impl AdminRpcHandler {
async fn handle_bucket_deny(&self, query: &PermBucketOpt) -> Result<AdminRpc, Error> { async fn handle_bucket_deny(&self, query: &PermBucketOpt) -> Result<AdminRpc, Error> {
let helper = self.garage.bucket_helper(); let helper = self.garage.bucket_helper();
let key_helper = self.garage.key_helper();
let bucket_id = helper let bucket_id = helper
.resolve_global_bucket_name(&query.bucket) .resolve_global_bucket_name(&query.bucket)
.await? .await?
.ok_or_bad_request("Bucket not found")?; .ok_or_bad_request("Bucket not found")?;
let key = helper.get_existing_matching_key(&query.key_pattern).await?; let key = key_helper
.get_existing_matching_key(&query.key_pattern)
.await?;
let allow_read = !query.read && key.allow_read(&bucket_id); let allow_read = !query.read && key.allow_read(&bucket_id);
let allow_write = !query.write && key.allow_write(&bucket_id); let allow_write = !query.write && key.allow_write(&bucket_id);
@ -469,7 +477,7 @@ impl AdminRpcHandler {
async fn handle_key_info(&self, query: &KeyOpt) -> Result<AdminRpc, Error> { async fn handle_key_info(&self, query: &KeyOpt) -> Result<AdminRpc, Error> {
let key = self let key = self
.garage .garage
.bucket_helper() .key_helper()
.get_existing_matching_key(&query.key_pattern) .get_existing_matching_key(&query.key_pattern)
.await?; .await?;
self.key_info_result(key).await self.key_info_result(key).await
@ -484,7 +492,7 @@ impl AdminRpcHandler {
async fn handle_rename_key(&self, query: &KeyRenameOpt) -> Result<AdminRpc, Error> { async fn handle_rename_key(&self, query: &KeyRenameOpt) -> Result<AdminRpc, Error> {
let mut key = self let mut key = self
.garage .garage
.bucket_helper() .key_helper()
.get_existing_matching_key(&query.key_pattern) .get_existing_matching_key(&query.key_pattern)
.await?; .await?;
key.params_mut() key.params_mut()
@ -496,9 +504,11 @@ impl AdminRpcHandler {
} }
async fn handle_delete_key(&self, query: &KeyDeleteOpt) -> Result<AdminRpc, Error> { async fn handle_delete_key(&self, query: &KeyDeleteOpt) -> Result<AdminRpc, Error> {
let helper = self.garage.bucket_helper(); let key_helper = self.garage.key_helper();
let mut key = helper.get_existing_matching_key(&query.key_pattern).await?; let mut key = key_helper
.get_existing_matching_key(&query.key_pattern)
.await?;
if !query.yes { if !query.yes {
return Err(Error::BadRequest( return Err(Error::BadRequest(
@ -506,32 +516,7 @@ impl AdminRpcHandler {
)); ));
} }
let state = key.state.as_option_mut().unwrap(); key_helper.delete_key(&mut key).await?;
// --- done checking, now commit ---
// (the step at unset_local_bucket_alias will fail if a bucket
// does not have another alias, the deletion will be
// interrupted in the middle if that happens)
// 1. Delete local aliases
for (alias, _, to) in state.local_aliases.items().iter() {
if let Some(bucket_id) = to {
helper
.unset_local_bucket_alias(*bucket_id, &key.key_id, alias)
.await?;
}
}
// 2. Remove permissions on all authorized buckets
for (ab_id, _auth) in state.authorized_buckets.items().iter() {
helper
.set_bucket_key_permissions(*ab_id, &key.key_id, BucketKeyPerm::NO_PERMISSIONS)
.await?;
}
// 3. Actually delete key
key.state = Deletable::delete();
self.garage.key_table.insert(&key).await?;
Ok(AdminRpc::Ok(format!( Ok(AdminRpc::Ok(format!(
"Key {} was deleted successfully.", "Key {} was deleted successfully.",
@ -542,7 +527,7 @@ impl AdminRpcHandler {
async fn handle_allow_key(&self, query: &KeyPermOpt) -> Result<AdminRpc, Error> { async fn handle_allow_key(&self, query: &KeyPermOpt) -> Result<AdminRpc, Error> {
let mut key = self let mut key = self
.garage .garage
.bucket_helper() .key_helper()
.get_existing_matching_key(&query.key_pattern) .get_existing_matching_key(&query.key_pattern)
.await?; .await?;
if query.create_bucket { if query.create_bucket {
@ -555,7 +540,7 @@ impl AdminRpcHandler {
async fn handle_deny_key(&self, query: &KeyPermOpt) -> Result<AdminRpc, Error> { async fn handle_deny_key(&self, query: &KeyPermOpt) -> Result<AdminRpc, Error> {
let mut key = self let mut key = self
.garage .garage
.bucket_helper() .key_helper()
.get_existing_matching_key(&query.key_pattern) .get_existing_matching_key(&query.key_pattern)
.await?; .await?;
if query.create_bucket { if query.create_bucket {

View file

@ -191,6 +191,10 @@ impl Garage {
pub fn bucket_helper(&self) -> helper::bucket::BucketHelper { pub fn bucket_helper(&self) -> helper::bucket::BucketHelper {
helper::bucket::BucketHelper(self) helper::bucket::BucketHelper(self)
} }
pub fn key_helper(&self) -> helper::key::KeyHelper {
helper::key::KeyHelper(self)
}
} }
#[cfg(feature = "k2v")] #[cfg(feature = "k2v")]

View file

@ -8,7 +8,7 @@ use crate::bucket_alias_table::*;
use crate::bucket_table::*; use crate::bucket_table::*;
use crate::garage::Garage; use crate::garage::Garage;
use crate::helper::error::*; use crate::helper::error::*;
use crate::key_table::{Key, KeyFilter}; use crate::helper::key::KeyHelper;
use crate::permission::BucketKeyPerm; use crate::permission::BucketKeyPerm;
pub struct BucketHelper<'a>(pub(crate) &'a Garage); pub struct BucketHelper<'a>(pub(crate) &'a Garage);
@ -77,60 +77,6 @@ impl<'a> BucketHelper<'a> {
)) ))
} }
/// Returns a Key if it is present in key table,
/// even if it is in deleted state. Querying a non-existing
/// key ID returns an internal error.
pub async fn get_internal_key(&self, key_id: &String) -> Result<Key, Error> {
Ok(self
.0
.key_table
.get(&EmptyKey, key_id)
.await?
.ok_or_message(format!("Key {} does not exist", key_id))?)
}
/// Returns a Key if it is present in key table,
/// only if it is in non-deleted state.
/// Querying a non-existing key ID or a deleted key
/// returns a bad request error.
pub async fn get_existing_key(&self, key_id: &String) -> Result<Key, Error> {
self.0
.key_table
.get(&EmptyKey, key_id)
.await?
.filter(|b| !b.state.is_deleted())
.ok_or_bad_request(format!("Key {} does not exist or has been deleted", key_id))
}
/// Returns a Key if it is present in key table,
/// looking it up by key ID or by a match on its name,
/// only if it is in non-deleted state.
/// Querying a non-existing key ID or a deleted key
/// returns a bad request error.
pub async fn get_existing_matching_key(&self, pattern: &str) -> Result<Key, Error> {
let candidates = self
.0
.key_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())),
10,
EnumerationOrder::Forward,
)
.await?
.into_iter()
.collect::<Vec<_>>();
if candidates.len() != 1 {
Err(Error::BadRequest(format!(
"{} matching keys",
candidates.len()
)))
} else {
Ok(candidates.into_iter().next().unwrap())
}
}
/// Sets a new alias for a bucket in global namespace. /// Sets a new alias for a bucket in global namespace.
/// This function fails if: /// This function fails if:
/// - alias name is not valid according to S3 spec /// - alias name is not valid according to S3 spec
@ -303,6 +249,8 @@ impl<'a> BucketHelper<'a> {
key_id: &String, key_id: &String,
alias_name: &String, alias_name: &String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let key_helper = KeyHelper(self.0);
if !is_valid_bucket_name(alias_name) { if !is_valid_bucket_name(alias_name) {
return Err(Error::BadRequest(format!( return Err(Error::BadRequest(format!(
"{}: {}", "{}: {}",
@ -311,7 +259,7 @@ impl<'a> BucketHelper<'a> {
} }
let mut bucket = self.get_existing_bucket(bucket_id).await?; let mut bucket = self.get_existing_bucket(bucket_id).await?;
let mut key = self.get_existing_key(key_id).await?; let mut key = key_helper.get_existing_key(key_id).await?;
let mut key_param = key.state.as_option_mut().unwrap(); let mut key_param = key.state.as_option_mut().unwrap();
@ -360,8 +308,10 @@ impl<'a> BucketHelper<'a> {
key_id: &String, key_id: &String,
alias_name: &String, alias_name: &String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let key_helper = KeyHelper(self.0);
let mut bucket = self.get_existing_bucket(bucket_id).await?; let mut bucket = self.get_existing_bucket(bucket_id).await?;
let mut key = self.get_existing_key(key_id).await?; let mut key = key_helper.get_existing_key(key_id).await?;
let mut bucket_p = bucket.state.as_option_mut().unwrap(); let mut bucket_p = bucket.state.as_option_mut().unwrap();
@ -429,8 +379,10 @@ impl<'a> BucketHelper<'a> {
key_id: &String, key_id: &String,
mut perm: BucketKeyPerm, mut perm: BucketKeyPerm,
) -> Result<(), Error> { ) -> Result<(), Error> {
let key_helper = KeyHelper(self.0);
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 = key_helper.get_internal_key(key_id).await?;
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) {

102
src/model/helper/key.rs Normal file
View file

@ -0,0 +1,102 @@
use garage_table::util::*;
use garage_util::crdt::*;
use garage_util::error::OkOrMessage;
use crate::garage::Garage;
use crate::helper::bucket::BucketHelper;
use crate::helper::error::*;
use crate::key_table::{Key, KeyFilter};
use crate::permission::BucketKeyPerm;
pub struct KeyHelper<'a>(pub(crate) &'a Garage);
#[allow(clippy::ptr_arg)]
impl<'a> KeyHelper<'a> {
/// Returns a Key if it is present in key table,
/// even if it is in deleted state. Querying a non-existing
/// key ID returns an internal error.
pub async fn get_internal_key(&self, key_id: &String) -> Result<Key, Error> {
Ok(self
.0
.key_table
.get(&EmptyKey, key_id)
.await?
.ok_or_message(format!("Key {} does not exist", key_id))?)
}
/// Returns a Key if it is present in key table,
/// only if it is in non-deleted state.
/// Querying a non-existing key ID or a deleted key
/// returns a bad request error.
pub async fn get_existing_key(&self, key_id: &String) -> Result<Key, Error> {
self.0
.key_table
.get(&EmptyKey, key_id)
.await?
.filter(|b| !b.state.is_deleted())
.ok_or_bad_request(format!("Key {} does not exist or has been deleted", key_id))
}
/// Returns a Key if it is present in key table,
/// looking it up by key ID or by a match on its name,
/// only if it is in non-deleted state.
/// Querying a non-existing key ID or a deleted key
/// returns a bad request error.
pub async fn get_existing_matching_key(&self, pattern: &str) -> Result<Key, Error> {
let candidates = self
.0
.key_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())),
10,
EnumerationOrder::Forward,
)
.await?
.into_iter()
.collect::<Vec<_>>();
if candidates.len() != 1 {
Err(Error::BadRequest(format!(
"{} matching keys",
candidates.len()
)))
} else {
Ok(candidates.into_iter().next().unwrap())
}
}
/// Deletes an API access key
pub async fn delete_key(&self, key: &mut Key) -> Result<(), Error> {
let bucket_helper = BucketHelper(self.0);
let state = key.state.as_option_mut().unwrap();
// --- done checking, now commit ---
// (the step at unset_local_bucket_alias will fail if a bucket
// does not have another alias, the deletion will be
// interrupted in the middle if that happens)
// 1. Delete local aliases
for (alias, _, to) in state.local_aliases.items().iter() {
if let Some(bucket_id) = to {
bucket_helper
.unset_local_bucket_alias(*bucket_id, &key.key_id, alias)
.await?;
}
}
// 2. Remove permissions on all authorized buckets
for (ab_id, _auth) in state.authorized_buckets.items().iter() {
bucket_helper
.set_bucket_key_permissions(*ab_id, &key.key_id, BucketKeyPerm::NO_PERMISSIONS)
.await?;
}
// 3. Actually delete key
key.state = Deletable::delete();
self.0.key_table.insert(key).await?;
Ok(())
}
}

View file

@ -1,2 +1,3 @@
pub mod bucket; pub mod bucket;
pub mod error; pub mod error;
pub mod key;