more complete admin API #298
5 changed files with 137 additions and 7 deletions
|
@ -456,3 +456,48 @@ or no alias at all.
|
||||||
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
|
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
|
||||||
|
|
||||||
Warning: this will delete all aliases associated with the bucket!
|
Warning: this will delete all aliases associated with the bucket!
|
||||||
|
|
||||||
|
|
||||||
|
## Operations on permissions for keys on buckets
|
||||||
|
|
||||||
|
### BucketAllowKey `POST /bucket/allow`
|
||||||
|
|
||||||
|
Allows a key to do read/write/owner operations on a bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags in `permissions` which have the value `true` will be activated.
|
||||||
|
Other flags will remain unchanged.
|
||||||
|
|
||||||
|
### BucketDenyKey `POST /bucket/deny`
|
||||||
|
|
||||||
|
Denies a key from doing read/write/owner operations on a bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"permissions": {
|
||||||
|
"read": false,
|
||||||
|
"write": false,
|
||||||
|
"owner": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags in `permissions` which have the value `true` will be deactivated.
|
||||||
|
Other flags will remain unchanged.
|
||||||
|
|
|
@ -147,6 +147,9 @@ impl ApiHandler for AdminApiServer {
|
||||||
}
|
}
|
||||||
Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await,
|
Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await,
|
||||||
Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await,
|
Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await,
|
||||||
|
// Bucket-key permissions
|
||||||
|
Endpoint::BucketAllowKey => handle_bucket_allow_key(&self.garage, req).await,
|
||||||
|
Endpoint::BucketDenyKey => handle_bucket_deny_key(&self.garage, req).await,
|
||||||
_ => Err(Error::NotImplemented(format!(
|
_ => Err(Error::NotImplemented(format!(
|
||||||
"Admin endpoint {} not implemented yet",
|
"Admin endpoint {} not implemented yet",
|
||||||
endpoint.name()
|
endpoint.name()
|
||||||
|
|
|
@ -16,7 +16,7 @@ use garage_model::garage::Garage;
|
||||||
use garage_model::permission::*;
|
use garage_model::permission::*;
|
||||||
use garage_model::s3::object_table::ObjectFilter;
|
use garage_model::s3::object_table::ObjectFilter;
|
||||||
|
|
||||||
use crate::admin::key::KeyBucketPermResult;
|
use crate::admin::key::ApiBucketKeyPerm;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ async fn bucket_info_results(
|
||||||
permissions: p
|
permissions: p
|
||||||
.authorized_buckets
|
.authorized_buckets
|
||||||
.get(&bucket.id)
|
.get(&bucket.id)
|
||||||
.map(|p| KeyBucketPermResult {
|
.map(|p| ApiBucketKeyPerm {
|
||||||
read: p.allow_read,
|
read: p.allow_read,
|
||||||
write: p.allow_write,
|
write: p.allow_write,
|
||||||
owner: p.allow_owner,
|
owner: p.allow_owner,
|
||||||
|
@ -212,7 +212,7 @@ struct GetBucketInfoKey {
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
#[serde(rename = "name")]
|
#[serde(rename = "name")]
|
||||||
name: String,
|
name: String,
|
||||||
permissions: KeyBucketPermResult,
|
permissions: ApiBucketKeyPerm,
|
||||||
#[serde(rename = "bucketLocalAliases")]
|
#[serde(rename = "bucketLocalAliases")]
|
||||||
bucket_local_aliases: Vec<String>,
|
bucket_local_aliases: Vec<String>,
|
||||||
}
|
}
|
||||||
|
@ -368,3 +368,76 @@ pub async fn handle_delete_bucket(
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(Body::empty())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_bucket_allow_key(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
handle_bucket_change_key_perm(garage, req, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_bucket_deny_key(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
handle_bucket_change_key_perm(garage, req, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_bucket_change_key_perm(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
new_perm_flag: bool,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<BucketKeyPermChangeRequest>(req).await?;
|
||||||
|
|
||||||
|
let id_hex = hex::decode(&req.bucket_id).ok_or_bad_request("Invalid bucket id")?;
|
||||||
|
let bucket_id = Uuid::try_from(&id_hex).ok_or_bad_request("Invalid bucket id")?;
|
||||||
|
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_helper()
|
||||||
|
.get_existing_bucket(bucket_id)
|
||||||
|
.await?;
|
||||||
|
let state = bucket.state.as_option().unwrap();
|
||||||
|
|
||||||
|
let key = garage
|
||||||
|
.key_helper()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_bucket_key_permissions(bucket.id, &key.key_id, perm)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_table
|
||||||
|
.get(&EmptyKey, &bucket.id)
|
||||||
|
.await?
|
||||||
|
.ok_or_internal_error("Bucket should now exist but doesn't")?;
|
||||||
|
bucket_info_results(garage, bucket).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BucketKeyPermChangeRequest {
|
||||||
|
#[serde(rename = "bucketId")]
|
||||||
|
bucket_id: String,
|
||||||
|
#[serde(rename = "accessKeyId")]
|
||||||
|
access_key_id: String,
|
||||||
|
permissions: ApiBucketKeyPerm,
|
||||||
|
}
|
||||||
|
|
|
@ -198,7 +198,7 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
|
||||||
permissions: key_state
|
permissions: key_state
|
||||||
.authorized_buckets
|
.authorized_buckets
|
||||||
.get(&bucket.id)
|
.get(&bucket.id)
|
||||||
.map(|p| KeyBucketPermResult {
|
.map(|p| ApiBucketKeyPerm {
|
||||||
read: p.allow_read,
|
read: p.allow_read,
|
||||||
write: p.allow_write,
|
write: p.allow_write,
|
||||||
owner: p.allow_owner,
|
owner: p.allow_owner,
|
||||||
|
@ -239,12 +239,15 @@ struct KeyInfoBucketResult {
|
||||||
global_aliases: Vec<String>,
|
global_aliases: Vec<String>,
|
||||||
#[serde(rename = "localAliases")]
|
#[serde(rename = "localAliases")]
|
||||||
local_aliases: Vec<String>,
|
local_aliases: Vec<String>,
|
||||||
permissions: KeyBucketPermResult,
|
permissions: ApiBucketKeyPerm,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
pub(crate) struct KeyBucketPermResult {
|
pub(crate) struct ApiBucketKeyPerm {
|
||||||
|
#[serde(default)]
|
||||||
pub(crate) read: bool,
|
pub(crate) read: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub(crate) write: bool,
|
pub(crate) write: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub(crate) owner: bool,
|
pub(crate) owner: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ pub enum Endpoint {
|
||||||
DeleteBucket {
|
DeleteBucket {
|
||||||
id: String,
|
id: String,
|
||||||
},
|
},
|
||||||
|
// Bucket-Key Permissions
|
||||||
|
BucketAllowKey,
|
||||||
|
BucketDenyKey,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
impl Endpoint {
|
impl Endpoint {
|
||||||
|
@ -81,6 +84,9 @@ impl Endpoint {
|
||||||
GET "/bucket" => ListBuckets,
|
GET "/bucket" => ListBuckets,
|
||||||
POST "/bucket" => CreateBucket,
|
POST "/bucket" => CreateBucket,
|
||||||
DELETE "/bucket" if id => DeleteBucket (query::id),
|
DELETE "/bucket" if id => DeleteBucket (query::id),
|
||||||
|
// Bucket-key permissions
|
||||||
|
POST "/bucket/allow" => BucketAllowKey,
|
||||||
|
POST "/bucket/deny" => BucketDenyKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if let Some(message) = query.nonempty_message() {
|
if let Some(message) = query.nonempty_message() {
|
||||||
|
|
Loading…
Reference in a new issue