garage/src/api/admin/bucket.rs
Alex Auvolat 5e7307cbf3
All checks were successful
ci/woodpecker/push/debug Pipeline was successful
ci/woodpecker/pr/debug Pipeline was successful
admin api: add comments for InspectObject
2025-04-06 14:21:54 +02:00

738 lines
18 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use chrono::DateTime;
use garage_util::crdt::*;
use garage_util::data::*;
use garage_util::time::*;
use garage_table::*;
use garage_model::bucket_alias_table::*;
use garage_model::bucket_table::*;
use garage_model::garage::Garage;
use garage_model::permission::*;
use garage_model::s3::mpu_table;
use garage_model::s3::object_table::*;
use garage_api_common::common_error::CommonError;
use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};
impl RequestHandler for ListBucketsRequest {
type Response = ListBucketsResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<ListBucketsResponse, Error> {
let buckets = garage
.bucket_table
.get_range(
&EmptyKey,
None,
Some(DeletedFilter::NotDeleted),
10000,
EnumerationOrder::Forward,
)
.await?;
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))
}
}
impl RequestHandler for GetBucketInfoRequest {
type Response = GetBucketInfoResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetBucketInfoResponse, Error> {
let bucket_id = match (self.id, self.global_alias, self.search) {
(Some(id), None, None) => parse_bucket_id(&id)?,
(None, Some(ga), None) => garage
.bucket_alias_table
.get(&EmptyKey, &ga)
.await?
.and_then(|x| *x.state.get())
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
(None, None, Some(search)) => {
let helper = garage.bucket_helper();
if let Some(bucket) = helper.resolve_global_bucket(&search).await? {
bucket.id
} else {
let hexdec = if search.len() >= 2 {
search
.get(..search.len() & !1)
.and_then(|x| hex::decode(x).ok())
} else {
None
};
let hex = hexdec
.ok_or_else(|| Error::Common(CommonError::NoSuchBucket(search.clone())))?;
let mut start = [0u8; 32];
start
.as_mut_slice()
.get_mut(..hex.len())
.ok_or_bad_request("invalid length")?
.copy_from_slice(&hex);
let mut candidates = garage
.bucket_table
.get_range(
&EmptyKey,
Some(start.into()),
Some(DeletedFilter::NotDeleted),
10,
EnumerationOrder::Forward,
)
.await?
.into_iter()
.collect::<Vec<_>>();
candidates.retain(|x| hex::encode(x.id).starts_with(&search));
if candidates.is_empty() {
return Err(Error::Common(CommonError::NoSuchBucket(search.clone())));
} else if candidates.len() == 1 {
candidates.into_iter().next().unwrap().id
} else {
return Err(Error::bad_request(format!(
"Several matching buckets: {}",
search
)));
}
}
}
_ => {
return Err(Error::bad_request(
"Either id, globalAlias or search must be provided (but not several of them)",
));
}
};
bucket_info_results(garage, bucket_id).await
}
}
impl RequestHandler for CreateBucketRequest {
type Response = CreateBucketResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<CreateBucketResponse, Error> {
let helper = garage.locked_helper().await;
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(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 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?,
))
}
}
impl RequestHandler for DeleteBucketRequest {
type Response = DeleteBucketResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> 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)
}
}
impl RequestHandler for UpdateBucketRequest {
type Response = UpdateBucketResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<UpdateBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.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) = self.body.website_access {
if wa.enabled {
let (redirect_all, routing_rules) = match state.website_config.get() {
Some(wc) => (wc.redirect_all.clone(), wc.routing_rules.clone()),
None => (None, Vec::new()),
};
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,
redirect_all,
routing_rules,
}));
} 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);
}
}
if let Some(q) = self.body.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?,
))
}
}
impl RequestHandler for CleanupIncompleteUploadsRequest {
type Response = CleanupIncompleteUploadsResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<CleanupIncompleteUploadsResponse, Error> {
let duration = Duration::from_secs(self.older_than_secs);
let bucket_id = parse_bucket_id(&self.bucket_id)?;
let count = garage
.bucket_helper()
.cleanup_incomplete_uploads(&bucket_id, duration)
.await?;
Ok(CleanupIncompleteUploadsResponse {
uploads_deleted: count as u64,
})
}
}
impl RequestHandler for InspectObjectRequest {
type Response = InspectObjectResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<InspectObjectResponse, Error> {
let bucket_id = parse_bucket_id(&self.bucket_id)?;
let object = garage
.object_table
.get(&bucket_id, &self.key)
.await?
.ok_or_else(|| Error::NoSuchKey)?;
let mut versions = vec![];
for obj_ver in object.versions().iter() {
let ver = garage.version_table.get(&obj_ver.uuid, &EmptyKey).await?;
let blocks = ver
.map(|v| {
v.blocks
.items()
.iter()
.map(|(vk, vb)| InspectObjectBlock {
part_number: vk.part_number,
offset: vk.offset,
hash: hex::encode(&vb.hash),
size: vb.size,
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let uuid = hex::encode(&obj_ver.uuid);
let timestamp = DateTime::from_timestamp_millis(obj_ver.timestamp as i64)
.expect("invalid timestamp in db");
match &obj_ver.state {
ObjectVersionState::Uploading { encryption, .. } => {
versions.push(InspectObjectVersion {
uuid,
timestamp,
encrypted: !matches!(encryption, ObjectVersionEncryption::Plaintext { .. }),
uploading: true,
headers: match encryption {
ObjectVersionEncryption::Plaintext { inner } => inner.headers.clone(),
_ => vec![],
},
blocks,
..Default::default()
});
}
ObjectVersionState::Complete(data) => match data {
ObjectVersionData::DeleteMarker => {
versions.push(InspectObjectVersion {
uuid,
timestamp,
delete_marker: true,
..Default::default()
});
}
ObjectVersionData::Inline(meta, _) => {
versions.push(InspectObjectVersion {
uuid,
timestamp,
inline: true,
size: Some(meta.size),
etag: Some(meta.etag.clone()),
encrypted: !matches!(
meta.encryption,
ObjectVersionEncryption::Plaintext { .. }
),
headers: match &meta.encryption {
ObjectVersionEncryption::Plaintext { inner } => {
inner.headers.clone()
}
_ => vec![],
},
..Default::default()
});
}
ObjectVersionData::FirstBlock(meta, _) => {
versions.push(InspectObjectVersion {
uuid,
timestamp,
size: Some(meta.size),
etag: Some(meta.etag.clone()),
encrypted: !matches!(
meta.encryption,
ObjectVersionEncryption::Plaintext { .. }
),
headers: match &meta.encryption {
ObjectVersionEncryption::Plaintext { inner } => {
inner.headers.clone()
}
_ => vec![],
},
blocks,
..Default::default()
});
}
},
ObjectVersionState::Aborted => {
versions.push(InspectObjectVersion {
uuid,
timestamp,
aborted: true,
blocks,
..Default::default()
});
}
}
}
Ok(InspectObjectResponse {
bucket_id: hex::encode(&object.bucket_id),
key: object.key,
versions,
})
}
}
// ---- BUCKET/KEY PERMISSIONS ----
impl RequestHandler for AllowBucketKeyRequest {
type Response = AllowBucketKeyResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<AllowBucketKeyResponse, Error> {
let res = handle_bucket_change_key_perm(garage, self.0, true).await?;
Ok(AllowBucketKeyResponse(res))
}
}
impl RequestHandler for DenyBucketKeyRequest {
type Response = DenyBucketKeyResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<DenyBucketKeyResponse, Error> {
let res = handle_bucket_change_key_perm(garage, self.0, false).await?;
Ok(DenyBucketKeyResponse(res))
}
}
pub async fn handle_bucket_change_key_perm(
garage: &Arc<Garage>,
req: BucketKeyPermChangeRequest,
new_perm_flag: bool,
) -> Result<GetBucketInfoResponse, Error> {
let helper = garage.locked_helper().await;
let bucket_id = parse_bucket_id(&req.bucket_id)?;
let bucket = helper.bucket().get_existing_bucket(bucket_id).await?;
let state = bucket.state.as_option().unwrap();
let key = helper.key().get_existing_key(&req.access_key_id).await?;
let mut perm = state
.authorized_keys
.get(&key.key_id)
.cloned()
.unwrap_or(BucketKeyPerm::NO_PERMISSIONS);
if req.permissions.read {
perm.allow_read = new_perm_flag;
}
if req.permissions.write {
perm.allow_write = new_perm_flag;
}
if req.permissions.owner {
perm.allow_owner = new_perm_flag;
}
helper
.set_bucket_key_permissions(bucket.id, &key.key_id, perm)
.await?;
bucket_info_results(garage, bucket.id).await
}
// ---- BUCKET ALIASES ----
impl RequestHandler for AddBucketAliasRequest {
type Response = AddBucketAliasResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<AddBucketAliasResponse, Error> {
let bucket_id = parse_bucket_id(&self.bucket_id)?;
let helper = garage.locked_helper().await;
match self.alias {
BucketAliasEnum::Global { global_alias } => {
helper
.set_global_bucket_alias(bucket_id, &global_alias)
.await?
}
BucketAliasEnum::Local {
local_alias,
access_key_id,
} => {
helper
.set_local_bucket_alias(bucket_id, &access_key_id, &local_alias)
.await?
}
}
Ok(AddBucketAliasResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
}
impl RequestHandler for RemoveBucketAliasRequest {
type Response = RemoveBucketAliasResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<RemoveBucketAliasResponse, Error> {
let bucket_id = parse_bucket_id(&self.bucket_id)?;
let helper = garage.locked_helper().await;
match self.alias {
BucketAliasEnum::Global { global_alias } => {
helper
.unset_global_bucket_alias(bucket_id, &global_alias)
.await?
}
BucketAliasEnum::Local {
local_alias,
access_key_id,
} => {
helper
.unset_local_bucket_alias(bucket_id, &access_key_id, &local_alias)
.await?
}
}
Ok(RemoveBucketAliasResponse(
bucket_info_results(garage, bucket_id).await?,
))
}
}
// ---- HELPER ----
async fn bucket_info_results(
garage: &Arc<Garage>,
bucket_id: Uuid,
) -> Result<GetBucketInfoResponse, Error> {
let bucket = garage
.bucket_helper()
.get_existing_bucket(bucket_id)
.await?;
let counters = garage
.object_counter_table
.table
.get(&bucket.id, &EmptyKey)
.await?
.map(|x| x.filtered_values(&garage.system.cluster_layout()))
.unwrap_or_default();
let mpu_counters = garage
.mpu_counter_table
.table
.get(&bucket.id, &EmptyKey)
.await?
.map(|x| x.filtered_values(&garage.system.cluster_layout()))
.unwrap_or_default();
let mut relevant_keys = HashMap::new();
for (k, _) in bucket
.state
.as_option()
.unwrap()
.authorized_keys
.items()
.iter()
{
if let Some(key) = garage
.key_table
.get(&EmptyKey, k)
.await?
.filter(|k| !k.is_deleted())
{
if !key.state.is_deleted() {
relevant_keys.insert(k.clone(), key);
}
}
}
for ((k, _), _, _) in bucket
.state
.as_option()
.unwrap()
.local_aliases
.items()
.iter()
{
if relevant_keys.contains_key(k) {
continue;
}
if let Some(key) = garage.key_table.get(&EmptyKey, k).await? {
if !key.state.is_deleted() {
relevant_keys.insert(k.clone(), key);
}
}
}
let state = bucket.state.as_option().unwrap();
let quotas = state.quotas.get();
let res = GetBucketInfoResponse {
id: hex::encode(bucket.id),
global_aliases: state
.aliases
.items()
.iter()
.filter(|(_, _, a)| *a)
.map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(),
website_access: state.website_config.get().is_some(),
website_config: state.website_config.get().clone().map(|wsc| {
GetBucketInfoWebsiteResponse {
index_document: wsc.index_document,
error_document: wsc.error_document,
}
}),
keys: relevant_keys
.into_values()
.map(|key| {
let p = key.state.as_option().unwrap();
GetBucketInfoKey {
access_key_id: key.key_id,
name: p.name.get().to_string(),
permissions: p
.authorized_buckets
.get(&bucket.id)
.map(|p| ApiBucketKeyPerm {
read: p.allow_read,
write: p.allow_write,
owner: p.allow_owner,
})
.unwrap_or_default(),
bucket_local_aliases: p
.local_aliases
.items()
.iter()
.filter(|(_, _, b)| *b == Some(bucket.id))
.map(|(n, _, _)| n.to_string())
.collect::<Vec<_>>(),
}
})
.collect::<Vec<_>>(),
objects: *counters.get(OBJECTS).unwrap_or(&0),
bytes: *counters.get(BYTES).unwrap_or(&0),
unfinished_uploads: *counters.get(UNFINISHED_UPLOADS).unwrap_or(&0),
unfinished_multipart_uploads: *mpu_counters.get(mpu_table::UPLOADS).unwrap_or(&0),
unfinished_multipart_upload_parts: *mpu_counters.get(mpu_table::PARTS).unwrap_or(&0),
unfinished_multipart_upload_bytes: *mpu_counters.get(mpu_table::BYTES).unwrap_or(&0),
quotas: ApiBucketQuotas {
max_size: quotas.max_size,
max_objects: quotas.max_objects,
},
};
Ok(res)
}
fn parse_bucket_id(id: &str) -> Result<Uuid, Error> {
let id_hex = hex::decode(id).ok_or_bad_request("Invalid bucket id")?;
Ok(Uuid::try_from(&id_hex).ok_or_bad_request("Invalid bucket id")?)
}