admin api: make all handlers impls of a single trait

This commit is contained in:
Alex Auvolat
2025-01-28 00:22:14 +01:00
parent c1eb1610ba
commit 831f2b0207
5 changed files with 781 additions and 522 deletions
+298 -252
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use async_trait::async_trait;
use garage_util::crdt::*;
use garage_util::data::*;
@@ -18,83 +18,93 @@ use garage_model::s3::object_table::*;
use crate::admin::api::ApiBucketKeyPerm;
use crate::admin::api::{
ApiBucketQuotas, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest,
GetBucketInfoKey, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, ListBucketsResponseItem,
UpdateBucketRequest,
ApiBucketQuotas, BucketAllowKeyRequest, BucketAllowKeyResponse, BucketDenyKeyRequest,
BucketDenyKeyResponse, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest,
CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, GetBucketInfoKey,
GetBucketInfoRequest, GetBucketInfoResponse, GetBucketInfoWebsiteResponse,
GlobalAliasBucketRequest, GlobalAliasBucketResponse, GlobalUnaliasBucketRequest,
GlobalUnaliasBucketResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem,
LocalAliasBucketRequest, LocalAliasBucketResponse, LocalUnaliasBucketRequest,
LocalUnaliasBucketResponse, UpdateBucketRequest, UpdateBucketResponse,
};
use crate::admin::api_server::ResBody;
use crate::admin::error::*;
use crate::admin::EndpointHandler;
use crate::common_error::CommonError;
use crate::helpers::*;
pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let buckets = garage
.bucket_table
.get_range(
&EmptyKey,
None,
Some(DeletedFilter::NotDeleted),
10000,
EnumerationOrder::Forward,
)
.await?;
#[async_trait]
impl EndpointHandler for ListBucketsRequest {
type Response = ListBucketsResponse;
let res = buckets
.into_iter()
.map(|b| {
let state = b.state.as_option().unwrap();
ListBucketsResponseItem {
id: hex::encode(b.id),
global_aliases: state
.aliases
.items()
.iter()
.filter(|(_, _, a)| *a)
.map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(),
local_aliases: state
.local_aliases
.items()
.iter()
.filter(|(_, _, a)| *a)
.map(|((k, n), _, _)| BucketLocalAlias {
access_key_id: k.to_string(),
alias: n.to_string(),
})
.collect::<Vec<_>>(),
}
})
.collect::<Vec<_>>();
async fn handle(self, garage: &Arc<Garage>) -> Result<ListBucketsResponse, Error> {
let buckets = garage
.bucket_table
.get_range(
&EmptyKey,
None,
Some(DeletedFilter::NotDeleted),
10000,
EnumerationOrder::Forward,
)
.await?;
Ok(json_ok_response(&res)?)
let res = buckets
.into_iter()
.map(|b| {
let state = b.state.as_option().unwrap();
ListBucketsResponseItem {
id: hex::encode(b.id),
global_aliases: state
.aliases
.items()
.iter()
.filter(|(_, _, a)| *a)
.map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(),
local_aliases: state
.local_aliases
.items()
.iter()
.filter(|(_, _, a)| *a)
.map(|((k, n), _, _)| BucketLocalAlias {
access_key_id: k.to_string(),
alias: n.to_string(),
})
.collect::<Vec<_>>(),
}
})
.collect::<Vec<_>>();
Ok(ListBucketsResponse(res))
}
}
pub async fn handle_get_bucket_info(
garage: &Arc<Garage>,
id: Option<String>,
global_alias: Option<String>,
) -> Result<Response<ResBody>, Error> {
let bucket_id = match (id, global_alias) {
(Some(id), None) => parse_bucket_id(&id)?,
(None, Some(ga)) => garage
.bucket_helper()
.resolve_global_bucket_name(&ga)
.await?
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
_ => {
return Err(Error::bad_request(
"Either id or globalAlias must be provided (but not both)",
));
}
};
#[async_trait]
impl EndpointHandler for GetBucketInfoRequest {
type Response = GetBucketInfoResponse;
bucket_info_results(garage, bucket_id).await
async fn handle(self, garage: &Arc<Garage>) -> Result<GetBucketInfoResponse, Error> {
let bucket_id = match (self.id, self.global_alias) {
(Some(id), None) => parse_bucket_id(&id)?,
(None, Some(ga)) => garage
.bucket_helper()
.resolve_global_bucket_name(&ga)
.await?
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
_ => {
return Err(Error::bad_request(
"Either id or globalAlias must be provided (but not both)",
));
}
};
bucket_info_results(garage, bucket_id).await
}
}
async fn bucket_info_results(
garage: &Arc<Garage>,
bucket_id: Uuid,
) -> Result<Response<ResBody>, Error> {
) -> Result<GetBucketInfoResponse, Error> {
let bucket = garage
.bucket_helper()
.get_existing_bucket(bucket_id)
@@ -211,181 +221,203 @@ async fn bucket_info_results(
},
};
Ok(json_ok_response(&res)?)
Ok(res)
}
pub async fn handle_create_bucket(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<CreateBucketRequest, _, Error>(req).await?;
#[async_trait]
impl EndpointHandler for CreateBucketRequest {
type Response = CreateBucketResponse;
let helper = garage.locked_helper().await;
async fn handle(self, garage: &Arc<Garage>) -> Result<CreateBucketResponse, Error> {
let helper = garage.locked_helper().await;
if let Some(ga) = &req.global_alias {
if !is_valid_bucket_name(ga) {
return Err(Error::bad_request(format!(
"{}: {}",
ga, INVALID_BUCKET_NAME_MESSAGE
)));
}
if let Some(ga) = &self.global_alias {
if !is_valid_bucket_name(ga) {
return Err(Error::bad_request(format!(
"{}: {}",
ga, INVALID_BUCKET_NAME_MESSAGE
)));
}
if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? {
if alias.state.get().is_some() {
return Err(CommonError::BucketAlreadyExists.into());
if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? {
if alias.state.get().is_some() {
return Err(CommonError::BucketAlreadyExists.into());
}
}
}
}
if let Some(la) = &req.local_alias {
if !is_valid_bucket_name(&la.alias) {
return Err(Error::bad_request(format!(
"{}: {}",
la.alias, INVALID_BUCKET_NAME_MESSAGE
)));
if let Some(la) = &self.local_alias {
if !is_valid_bucket_name(&la.alias) {
return Err(Error::bad_request(format!(
"{}: {}",
la.alias, INVALID_BUCKET_NAME_MESSAGE
)));
}
let key = helper.key().get_existing_key(&la.access_key_id).await?;
let state = key.state.as_option().unwrap();
if matches!(state.local_aliases.get(&la.alias), Some(_)) {
return Err(Error::bad_request("Local alias already exists"));
}
}
let key = helper.key().get_existing_key(&la.access_key_id).await?;
let state = key.state.as_option().unwrap();
if matches!(state.local_aliases.get(&la.alias), Some(_)) {
return Err(Error::bad_request("Local alias already exists"));
let bucket = Bucket::new();
garage.bucket_table.insert(&bucket).await?;
if let Some(ga) = &self.global_alias {
helper.set_global_bucket_alias(bucket.id, ga).await?;
}
if let Some(la) = &self.local_alias {
helper
.set_local_bucket_alias(bucket.id, &la.access_key_id, &la.alias)
.await?;
if la.allow.read || la.allow.write || la.allow.owner {
helper
.set_bucket_key_permissions(
bucket.id,
&la.access_key_id,
BucketKeyPerm {
timestamp: now_msec(),
allow_read: la.allow.read,
allow_write: la.allow.write,
allow_owner: la.allow.owner,
},
)
.await?;
}
}
Ok(CreateBucketResponse(
bucket_info_results(garage, bucket.id).await?,
))
}
}
let bucket = Bucket::new();
garage.bucket_table.insert(&bucket).await?;
#[async_trait]
impl EndpointHandler for DeleteBucketRequest {
type Response = DeleteBucketResponse;
if let Some(ga) = &req.global_alias {
helper.set_global_bucket_alias(bucket.id, ga).await?;
async fn handle(self, garage: &Arc<Garage>) -> Result<DeleteBucketResponse, Error> {
let helper = garage.locked_helper().await;
let bucket_id = parse_bucket_id(&self.id)?;
let mut bucket = helper.bucket().get_existing_bucket(bucket_id).await?;
let state = bucket.state.as_option().unwrap();
// Check bucket is empty
if !helper.bucket().is_bucket_empty(bucket_id).await? {
return Err(CommonError::BucketNotEmpty.into());
}
// --- done checking, now commit ---
// 1. delete authorization from keys that had access
for (key_id, perm) in bucket.authorized_keys() {
if perm.is_any() {
helper
.set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS)
.await?;
}
}
// 2. delete all local aliases
for ((key_id, alias), _, active) in state.local_aliases.items().iter() {
if *active {
helper
.unset_local_bucket_alias(bucket.id, key_id, alias)
.await?;
}
}
// 3. delete all global aliases
for (alias, _, active) in state.aliases.items().iter() {
if *active {
helper.purge_global_bucket_alias(bucket.id, alias).await?;
}
}
// 4. delete bucket
bucket.state = Deletable::delete();
garage.bucket_table.insert(&bucket).await?;
Ok(DeleteBucketResponse)
}
}
if let Some(la) = &req.local_alias {
helper
.set_local_bucket_alias(bucket.id, &la.access_key_id, &la.alias)
#[async_trait]
impl EndpointHandler for UpdateBucketRequest {
type Response = UpdateBucketResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<UpdateBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?;
let mut bucket = garage
.bucket_helper()
.get_existing_bucket(bucket_id)
.await?;
if la.allow.read || la.allow.write || la.allow.owner {
helper
.set_bucket_key_permissions(
bucket.id,
&la.access_key_id,
BucketKeyPerm {
timestamp: now_msec(),
allow_read: la.allow.read,
allow_write: la.allow.write,
allow_owner: la.allow.owner,
},
)
.await?;
}
}
let state = bucket.state.as_option_mut().unwrap();
bucket_info_results(garage, bucket.id).await
}
pub async fn handle_delete_bucket(
garage: &Arc<Garage>,
id: String,
) -> Result<Response<ResBody>, Error> {
let helper = garage.locked_helper().await;
let bucket_id = parse_bucket_id(&id)?;
let mut bucket = helper.bucket().get_existing_bucket(bucket_id).await?;
let state = bucket.state.as_option().unwrap();
// Check bucket is empty
if !helper.bucket().is_bucket_empty(bucket_id).await? {
return Err(CommonError::BucketNotEmpty.into());
}
// --- done checking, now commit ---
// 1. delete authorization from keys that had access
for (key_id, perm) in bucket.authorized_keys() {
if perm.is_any() {
helper
.set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS)
.await?;
}
}
// 2. delete all local aliases
for ((key_id, alias), _, active) in state.local_aliases.items().iter() {
if *active {
helper
.unset_local_bucket_alias(bucket.id, key_id, alias)
.await?;
}
}
// 3. delete all global aliases
for (alias, _, active) in state.aliases.items().iter() {
if *active {
helper.purge_global_bucket_alias(bucket.id, alias).await?;
}
}
// 4. delete bucket
bucket.state = Deletable::delete();
garage.bucket_table.insert(&bucket).await?;
Ok(Response::builder()
.status(StatusCode::NO_CONTENT)
.body(empty_body())?)
}
pub async fn handle_update_bucket(
garage: &Arc<Garage>,
id: String,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<UpdateBucketRequest, _, Error>(req).await?;
let bucket_id = parse_bucket_id(&id)?;
let mut bucket = garage
.bucket_helper()
.get_existing_bucket(bucket_id)
.await?;
let state = bucket.state.as_option_mut().unwrap();
if let Some(wa) = req.website_access {
if wa.enabled {
state.website_config.update(Some(WebsiteConfig {
index_document: wa.index_document.ok_or_bad_request(
"Please specify indexDocument when enabling website access.",
)?,
error_document: wa.error_document,
}));
} else {
if wa.index_document.is_some() || wa.error_document.is_some() {
return Err(Error::bad_request(
"Cannot specify indexDocument or errorDocument when disabling website access.",
));
if let Some(wa) = self.params.website_access {
if wa.enabled {
state.website_config.update(Some(WebsiteConfig {
index_document: wa.index_document.ok_or_bad_request(
"Please specify indexDocument when enabling website access.",
)?,
error_document: wa.error_document,
}));
} else {
if wa.index_document.is_some() || wa.error_document.is_some() {
return Err(Error::bad_request(
"Cannot specify indexDocument or errorDocument when disabling website access.",
));
}
state.website_config.update(None);
}
state.website_config.update(None);
}
if let Some(q) = self.params.quotas {
state.quotas.update(BucketQuotas {
max_size: q.max_size,
max_objects: q.max_objects,
});
}
garage.bucket_table.insert(&bucket).await?;
Ok(UpdateBucketResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
if let Some(q) = req.quotas {
state.quotas.update(BucketQuotas {
max_size: q.max_size,
max_objects: q.max_objects,
});
}
garage.bucket_table.insert(&bucket).await?;
bucket_info_results(garage, bucket_id).await
}
// ---- BUCKET/KEY PERMISSIONS ----
#[async_trait]
impl EndpointHandler for BucketAllowKeyRequest {
type Response = BucketAllowKeyResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<BucketAllowKeyResponse, Error> {
let res = handle_bucket_change_key_perm(garage, self.0, true).await?;
Ok(BucketAllowKeyResponse(res))
}
}
#[async_trait]
impl EndpointHandler for BucketDenyKeyRequest {
type Response = BucketDenyKeyResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<BucketDenyKeyResponse, Error> {
let res = handle_bucket_change_key_perm(garage, self.0, false).await?;
Ok(BucketDenyKeyResponse(res))
}
}
pub async fn handle_bucket_change_key_perm(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
req: BucketKeyPermChangeRequest,
new_perm_flag: bool,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<BucketKeyPermChangeRequest, _, Error>(req).await?;
) -> Result<GetBucketInfoResponse, Error> {
let helper = garage.locked_helper().await;
let bucket_id = parse_bucket_id(&req.bucket_id)?;
@@ -420,66 +452,80 @@ pub async fn handle_bucket_change_key_perm(
// ---- BUCKET ALIASES ----
pub async fn handle_global_alias_bucket(
garage: &Arc<Garage>,
bucket_id: String,
alias: String,
) -> Result<Response<ResBody>, Error> {
let bucket_id = parse_bucket_id(&bucket_id)?;
#[async_trait]
impl EndpointHandler for GlobalAliasBucketRequest {
type Response = GlobalAliasBucketResponse;
let helper = garage.locked_helper().await;
async fn handle(self, garage: &Arc<Garage>) -> Result<GlobalAliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?;
helper.set_global_bucket_alias(bucket_id, &alias).await?;
let helper = garage.locked_helper().await;
bucket_info_results(garage, bucket_id).await
helper
.set_global_bucket_alias(bucket_id, &self.alias)
.await?;
Ok(GlobalAliasBucketResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
}
pub async fn handle_global_unalias_bucket(
garage: &Arc<Garage>,
bucket_id: String,
alias: String,
) -> Result<Response<ResBody>, Error> {
let bucket_id = parse_bucket_id(&bucket_id)?;
#[async_trait]
impl EndpointHandler for GlobalUnaliasBucketRequest {
type Response = GlobalUnaliasBucketResponse;
let helper = garage.locked_helper().await;
async fn handle(self, garage: &Arc<Garage>) -> Result<GlobalUnaliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?;
helper.unset_global_bucket_alias(bucket_id, &alias).await?;
let helper = garage.locked_helper().await;
bucket_info_results(garage, bucket_id).await
helper
.unset_global_bucket_alias(bucket_id, &self.alias)
.await?;
Ok(GlobalUnaliasBucketResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
}
pub async fn handle_local_alias_bucket(
garage: &Arc<Garage>,
bucket_id: String,
access_key_id: String,
alias: String,
) -> Result<Response<ResBody>, Error> {
let bucket_id = parse_bucket_id(&bucket_id)?;
#[async_trait]
impl EndpointHandler for LocalAliasBucketRequest {
type Response = LocalAliasBucketResponse;
let helper = garage.locked_helper().await;
async fn handle(self, garage: &Arc<Garage>) -> Result<LocalAliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?;
helper
.set_local_bucket_alias(bucket_id, &access_key_id, &alias)
.await?;
let helper = garage.locked_helper().await;
bucket_info_results(garage, bucket_id).await
helper
.set_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias)
.await?;
Ok(LocalAliasBucketResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
}
pub async fn handle_local_unalias_bucket(
garage: &Arc<Garage>,
bucket_id: String,
access_key_id: String,
alias: String,
) -> Result<Response<ResBody>, Error> {
let bucket_id = parse_bucket_id(&bucket_id)?;
#[async_trait]
impl EndpointHandler for LocalUnaliasBucketRequest {
type Response = LocalUnaliasBucketResponse;
let helper = garage.locked_helper().await;
async fn handle(self, garage: &Arc<Garage>) -> Result<LocalUnaliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?;
helper
.unset_local_bucket_alias(bucket_id, &access_key_id, &alias)
.await?;
let helper = garage.locked_helper().await;
bucket_info_results(garage, bucket_id).await
helper
.unset_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias)
.await?;
Ok(LocalUnaliasBucketResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
}
// ---- HELPER ----