First key endpoints: ListKeys and GetKeyInfo
This commit is contained in:
parent
f97a7845e9
commit
5c00c9fb46
5 changed files with 268 additions and 5 deletions
|
@ -219,7 +219,16 @@ Returns all API access keys in the cluster.
|
|||
Example response:
|
||||
|
||||
```json
|
||||
#TODO
|
||||
[
|
||||
{
|
||||
"id": "GK31c2f218a2e44f485b94239e",
|
||||
"name": "test"
|
||||
},
|
||||
{
|
||||
"id": "GKe10061ac9c2921f09e4c5540",
|
||||
"name": "test2"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### CreateKey `POST /key`
|
||||
|
@ -235,13 +244,75 @@ Request body format:
|
|||
```
|
||||
|
||||
### GetKeyInfo `GET /key?id=<acces key id>`
|
||||
### GetKeyInfo `GET /key?search=<pattern>`
|
||||
|
||||
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:
|
||||
|
||||
```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>`
|
||||
|
|
|
@ -19,6 +19,7 @@ use crate::error::*;
|
|||
use crate::generic_server::*;
|
||||
|
||||
use crate::admin::cluster::*;
|
||||
use crate::admin::key::*;
|
||||
use crate::admin::router::{Authorization, Endpoint};
|
||||
|
||||
pub struct AdminApiServer {
|
||||
|
@ -125,10 +126,16 @@ impl ApiHandler for AdminApiServer {
|
|||
Endpoint::Options => self.handle_options(&req),
|
||||
Endpoint::Metrics => self.handle_metrics(),
|
||||
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
||||
// Layout
|
||||
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
||||
Endpoint::UpdateClusterLayout => handle_update_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,
|
||||
// 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!(
|
||||
"Admin endpoint {} not implemented yet",
|
||||
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 cluster;
|
||||
mod key;
|
||||
|
|
|
@ -25,7 +25,8 @@ pub enum Endpoint {
|
|||
ListKeys,
|
||||
CreateKey,
|
||||
GetKeyInfo {
|
||||
id: String,
|
||||
id: Option<String>,
|
||||
search: Option<String>,
|
||||
},
|
||||
DeleteKey {
|
||||
id: String,
|
||||
|
@ -56,7 +57,8 @@ impl Endpoint {
|
|||
POST "/layout/apply" => ApplyClusterLayout,
|
||||
POST "/layout/revert" => RevertClusterLayout,
|
||||
// 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" => CreateKey,
|
||||
DELETE "/key" if id => DeleteKey (query::id),
|
||||
|
@ -79,5 +81,6 @@ impl Endpoint {
|
|||
}
|
||||
|
||||
generateQueryParameters! {
|
||||
"id" => id
|
||||
"id" => id,
|
||||
"search" => search
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue