more complete admin API #298
5 changed files with 268 additions and 5 deletions
|
@ -219,7 +219,16 @@ Returns all API access keys in the cluster.
|
||||||
Example response:
|
Example response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
#TODO
|
[
|
||||||
|
{
|
||||||
|
"id": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"name": "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GKe10061ac9c2921f09e4c5540",
|
||||||
|
"name": "test2"
|
||||||
|
}
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### CreateKey `POST /key`
|
### CreateKey `POST /key`
|
||||||
|
@ -235,13 +244,75 @@ Request body format:
|
||||||
```
|
```
|
||||||
|
|
||||||
### GetKeyInfo `GET /key?id=<acces key id>`
|
### GetKeyInfo `GET /key?id=<acces key id>`
|
||||||
|
### GetKeyInfo `GET /key?search=<pattern>`
|
||||||
|
|
||||||
Returns information about the requested API access key.
|
Returns information about the requested API access key.
|
||||||
|
|
||||||
|
If `id` is set, the key is looked up using its exact identifier (faster).
|
||||||
|
If `search` is set, the key is looked up using its name or prefix
|
||||||
|
of identifier (slower, all keys are enumerated to do this).
|
||||||
|
|
||||||
Example response:
|
Example response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
#TODO
|
{
|
||||||
|
"name": "test",
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835",
|
||||||
|
"permissions": {
|
||||||
|
"createBucket": false
|
||||||
|
},
|
||||||
|
"buckets": [
|
||||||
|
{
|
||||||
|
"id": "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033",
|
||||||
|
"globalAliases": [
|
||||||
|
"test2"
|
||||||
|
],
|
||||||
|
"localAliases": [],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995",
|
||||||
|
"globalAliases": [
|
||||||
|
"test3"
|
||||||
|
],
|
||||||
|
"localAliases": [],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"globalAliases": [],
|
||||||
|
"localAliases": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95",
|
||||||
|
"globalAliases": [
|
||||||
|
"alex"
|
||||||
|
],
|
||||||
|
"localAliases": [],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### DeleteKey `DELETE /key?id=<acces key id>`
|
### DeleteKey `DELETE /key?id=<acces key id>`
|
||||||
|
|
|
@ -19,6 +19,7 @@ use crate::error::*;
|
||||||
use crate::generic_server::*;
|
use crate::generic_server::*;
|
||||||
|
|
||||||
use crate::admin::cluster::*;
|
use crate::admin::cluster::*;
|
||||||
|
use crate::admin::key::*;
|
||||||
use crate::admin::router::{Authorization, Endpoint};
|
use crate::admin::router::{Authorization, Endpoint};
|
||||||
|
|
||||||
pub struct AdminApiServer {
|
pub struct AdminApiServer {
|
||||||
|
@ -125,10 +126,16 @@ impl ApiHandler for AdminApiServer {
|
||||||
Endpoint::Options => self.handle_options(&req),
|
Endpoint::Options => self.handle_options(&req),
|
||||||
Endpoint::Metrics => self.handle_metrics(),
|
Endpoint::Metrics => self.handle_metrics(),
|
||||||
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
||||||
|
// Layout
|
||||||
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
||||||
Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await,
|
Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await,
|
||||||
Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await,
|
Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await,
|
||||||
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await,
|
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await,
|
||||||
|
// Keys
|
||||||
|
Endpoint::ListKeys => handle_list_keys(&self.garage).await,
|
||||||
|
Endpoint::GetKeyInfo { id, search } => {
|
||||||
|
handle_get_key_info(&self.garage, id, search).await
|
||||||
|
}
|
||||||
_ => Err(Error::NotImplemented(format!(
|
_ => Err(Error::NotImplemented(format!(
|
||||||
"Admin endpoint {} not implemented yet",
|
"Admin endpoint {} not implemented yet",
|
||||||
endpoint.name()
|
endpoint.name()
|
||||||
|
|
181
src/api/admin/key.rs
Normal file
181
src/api/admin/key.rs
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use hyper::{Body, Request, Response, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use garage_util::crdt::*;
|
||||||
|
use garage_util::data::*;
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
use garage_rpc::layout::*;
|
||||||
|
|
||||||
|
use garage_table::*;
|
||||||
|
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
use garage_model::key_table::*;
|
||||||
|
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::helpers::*;
|
||||||
|
|
||||||
|
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
|
let res = garage
|
||||||
|
.key_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||||
|
10000,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|k| ListKeyResultItem {
|
||||||
|
id: k.key_id.to_string(),
|
||||||
|
name: k.params().unwrap().name.get().clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ListKeyResultItem {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_key_info(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: Option<String>,
|
||||||
|
search: Option<String>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let key = if let Some(id) = id {
|
||||||
|
garage
|
||||||
|
.key_table
|
||||||
|
.get(&EmptyKey, &id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NoSuchKey)?
|
||||||
|
} else if let Some(search) = search {
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.get_existing_matching_key(&search)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::NoSuchKey)?
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut relevant_buckets = HashMap::new();
|
||||||
|
|
||||||
|
let key_state = key.state.as_option().unwrap();
|
||||||
|
|
||||||
|
for id in key_state
|
||||||
|
.authorized_buckets
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.chain(
|
||||||
|
key_state
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, _, v)| v.as_ref()),
|
||||||
|
) {
|
||||||
|
if !relevant_buckets.contains_key(id) {
|
||||||
|
if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? {
|
||||||
|
if b.state.as_option().is_some() {
|
||||||
|
relevant_buckets.insert(*id, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = GetKeyInfoResult {
|
||||||
|
name: key_state.name.get().clone(),
|
||||||
|
access_key_id: key.key_id.clone(),
|
||||||
|
secret_access_key: key_state.secret_key.clone(),
|
||||||
|
permissions: KeyPermResult {
|
||||||
|
create_bucket: *key_state.allow_create_bucket.get(),
|
||||||
|
},
|
||||||
|
buckets: relevant_buckets
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, bucket)| {
|
||||||
|
let state = bucket.state.as_option().unwrap();
|
||||||
|
KeyInfoBucketResult {
|
||||||
|
id: hex::encode(bucket.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(|((k, _), _, a)| *a && *k == key.key_id)
|
||||||
|
.map(|((_, n), _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
permissions: key_state
|
||||||
|
.authorized_buckets
|
||||||
|
.get(&bucket.id)
|
||||||
|
.map(|p| KeyBucketPermResult {
|
||||||
|
read: p.allow_read,
|
||||||
|
write: p.allow_write,
|
||||||
|
owner: p.allow_owner,
|
||||||
|
})
|
||||||
|
.unwrap_or(KeyBucketPermResult {
|
||||||
|
read: false,
|
||||||
|
write: false,
|
||||||
|
owner: false,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GetKeyInfoResult {
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "accessKeyId")]
|
||||||
|
access_key_id: String,
|
||||||
|
#[serde(rename = "secretAccessKey")]
|
||||||
|
secret_access_key: String,
|
||||||
|
permissions: KeyPermResult,
|
||||||
|
buckets: Vec<KeyInfoBucketResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct KeyPermResult {
|
||||||
|
#[serde(rename = "createBucket")]
|
||||||
|
create_bucket: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct KeyInfoBucketResult {
|
||||||
|
id: String,
|
||||||
|
#[serde(rename = "globalAliases")]
|
||||||
|
global_aliases: Vec<String>,
|
||||||
|
#[serde(rename = "localAliases")]
|
||||||
|
local_aliases: Vec<String>,
|
||||||
|
permissions: KeyBucketPermResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct KeyBucketPermResult {
|
||||||
|
read: bool,
|
||||||
|
write: bool,
|
||||||
|
owner: bool,
|
||||||
|
}
|
|
@ -2,3 +2,4 @@ pub mod api_server;
|
||||||
mod router;
|
mod router;
|
||||||
|
|
||||||
mod cluster;
|
mod cluster;
|
||||||
|
mod key;
|
||||||
|
|
|
@ -25,7 +25,8 @@ pub enum Endpoint {
|
||||||
ListKeys,
|
ListKeys,
|
||||||
CreateKey,
|
CreateKey,
|
||||||
GetKeyInfo {
|
GetKeyInfo {
|
||||||
id: String,
|
id: Option<String>,
|
||||||
|
search: Option<String>,
|
||||||
},
|
},
|
||||||
DeleteKey {
|
DeleteKey {
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -56,7 +57,8 @@ impl Endpoint {
|
||||||
POST "/layout/apply" => ApplyClusterLayout,
|
POST "/layout/apply" => ApplyClusterLayout,
|
||||||
POST "/layout/revert" => RevertClusterLayout,
|
POST "/layout/revert" => RevertClusterLayout,
|
||||||
// API key endpoints
|
// API key endpoints
|
||||||
GET "/key" if id => GetKeyInfo (query::id),
|
GET "/key" if id => GetKeyInfo (query_opt::id, query_opt::search),
|
||||||
|
GET "/key" if search => GetKeyInfo (query_opt::id, query_opt::search),
|
||||||
POST "/key" if id => UpdateKey (query::id),
|
POST "/key" if id => UpdateKey (query::id),
|
||||||
POST "/key" => CreateKey,
|
POST "/key" => CreateKey,
|
||||||
DELETE "/key" if id => DeleteKey (query::id),
|
DELETE "/key" if id => DeleteKey (query::id),
|
||||||
|
@ -79,5 +81,6 @@ impl Endpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
generateQueryParameters! {
|
generateQueryParameters! {
|
||||||
"id" => id
|
"id" => id,
|
||||||
|
"search" => search
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue