admin api: add functions to manage admin api tokens
This commit is contained in:
parent
ff6ec62d54
commit
d067a40b3f
11 changed files with 319 additions and 38 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1298,6 +1298,7 @@ dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytesize",
|
"bytesize",
|
||||||
|
"chrono",
|
||||||
"err-derive",
|
"err-derive",
|
||||||
"format_table",
|
"format_table",
|
||||||
"futures",
|
"futures",
|
||||||
|
|
|
@ -48,7 +48,7 @@ blake2 = "0.10"
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
bytesize = "1.1"
|
bytesize = "1.1"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
crc32fast = "1.4"
|
crc32fast = "1.4"
|
||||||
crc32c = "0.6"
|
crc32c = "0.6"
|
||||||
crypto-common = "0.1"
|
crypto-common = "0.1"
|
||||||
|
|
|
@ -25,6 +25,7 @@ garage_api_common.workspace = true
|
||||||
argon2.workspace = true
|
argon2.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
err-derive.workspace = true
|
err-derive.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
paste.workspace = true
|
paste.workspace = true
|
||||||
|
|
193
src/api/admin/admin_token.rs
Normal file
193
src/api/admin/admin_token.rs
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use garage_table::*;
|
||||||
|
use garage_util::time::now_msec;
|
||||||
|
|
||||||
|
use garage_model::admin_token_table::*;
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
|
||||||
|
use crate::api::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::{Admin, RequestHandler};
|
||||||
|
|
||||||
|
impl RequestHandler for ListAdminTokensRequest {
|
||||||
|
type Response = ListAdminTokensResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<ListAdminTokensResponse, Error> {
|
||||||
|
let now = now_msec();
|
||||||
|
|
||||||
|
let res = garage
|
||||||
|
.admin_token_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||||
|
10000,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|t| admin_token_info_results(t, now))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(ListAdminTokensResponse(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for GetAdminTokenInfoRequest {
|
||||||
|
type Response = GetAdminTokenInfoResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<GetAdminTokenInfoResponse, Error> {
|
||||||
|
let token = match (self.id, self.search) {
|
||||||
|
(Some(id), None) => get_existing_admin_token(garage, &id).await?,
|
||||||
|
(None, Some(search)) => {
|
||||||
|
let candidates = garage
|
||||||
|
.admin_token_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||||
|
10,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if candidates.len() != 1 {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"{} matching admin tokens",
|
||||||
|
candidates.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
candidates.into_iter().next().unwrap()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::bad_request(
|
||||||
|
"Either id or search must be provided (but not both)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(admin_token_info_results(&token, now_msec()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for CreateAdminTokenRequest {
|
||||||
|
type Response = CreateAdminTokenResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<CreateAdminTokenResponse, Error> {
|
||||||
|
let (mut token, secret) = if self.0.name.is_some() {
|
||||||
|
AdminApiToken::new("")
|
||||||
|
} else {
|
||||||
|
AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
|
||||||
|
};
|
||||||
|
|
||||||
|
apply_token_updates(&mut token, self.0);
|
||||||
|
|
||||||
|
garage.admin_token_table.insert(&token).await?;
|
||||||
|
|
||||||
|
Ok(CreateAdminTokenResponse {
|
||||||
|
secret_token: secret,
|
||||||
|
info: admin_token_info_results(&token, now_msec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for UpdateAdminTokenRequest {
|
||||||
|
type Response = UpdateAdminTokenResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<UpdateAdminTokenResponse, Error> {
|
||||||
|
let mut token = get_existing_admin_token(&garage, &self.id).await?;
|
||||||
|
|
||||||
|
apply_token_updates(&mut token, self.body);
|
||||||
|
|
||||||
|
garage.admin_token_table.insert(&token).await?;
|
||||||
|
|
||||||
|
Ok(UpdateAdminTokenResponse(admin_token_info_results(
|
||||||
|
&token,
|
||||||
|
now_msec(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for DeleteAdminTokenRequest {
|
||||||
|
type Response = DeleteAdminTokenResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<DeleteAdminTokenResponse, Error> {
|
||||||
|
let token = get_existing_admin_token(&garage, &self.id).await?;
|
||||||
|
|
||||||
|
garage
|
||||||
|
.admin_token_table
|
||||||
|
.insert(&AdminApiToken::delete(token.prefix))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(DeleteAdminTokenResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse {
|
||||||
|
let params = token.params().unwrap();
|
||||||
|
|
||||||
|
GetAdminTokenInfoResponse {
|
||||||
|
id: token.prefix.clone(),
|
||||||
|
name: params.name.get().to_string(),
|
||||||
|
expiration: params.expiration.get().map(|x| {
|
||||||
|
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||||
|
}),
|
||||||
|
expired: params
|
||||||
|
.expiration
|
||||||
|
.get()
|
||||||
|
.map(|exp| now > exp)
|
||||||
|
.unwrap_or(false),
|
||||||
|
scope: params.scope.get().0.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminApiToken, Error> {
|
||||||
|
garage
|
||||||
|
.admin_token_table
|
||||||
|
.get(&EmptyKey, id)
|
||||||
|
.await?
|
||||||
|
.filter(|k| !k.state.is_deleted())
|
||||||
|
.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_token_updates(token: &mut AdminApiToken, updates: UpdateAdminTokenRequestBody) {
|
||||||
|
let params = token.params_mut().unwrap();
|
||||||
|
|
||||||
|
if let Some(name) = updates.name {
|
||||||
|
params.name.update(name);
|
||||||
|
}
|
||||||
|
if let Some(expiration) = updates.expiration {
|
||||||
|
params
|
||||||
|
.expiration
|
||||||
|
.update(Some(expiration.timestamp_millis() as u64));
|
||||||
|
}
|
||||||
|
if let Some(scope) = updates.scope {
|
||||||
|
params.scope.update(AdminApiTokenScope(scope));
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,13 @@ admin_endpoints![
|
||||||
GetClusterStatistics,
|
GetClusterStatistics,
|
||||||
ConnectClusterNodes,
|
ConnectClusterNodes,
|
||||||
|
|
||||||
|
// Admin tokens operations
|
||||||
|
ListAdminTokens,
|
||||||
|
GetAdminTokenInfo,
|
||||||
|
CreateAdminToken,
|
||||||
|
UpdateAdminToken,
|
||||||
|
DeleteAdminToken,
|
||||||
|
|
||||||
// Layout operations
|
// Layout operations
|
||||||
GetClusterLayout,
|
GetClusterLayout,
|
||||||
GetClusterLayoutHistory,
|
GetClusterLayoutHistory,
|
||||||
|
@ -282,6 +289,84 @@ pub struct ConnectNodeResponse {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **********************************************
|
||||||
|
// Admin token operations
|
||||||
|
// **********************************************
|
||||||
|
|
||||||
|
// ---- ListAdminTokens ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ListAdminTokensRequest;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ListAdminTokensResponse(pub Vec<GetAdminTokenInfoResponse>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ListAdminTokensResponseItem {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GetAdminTokenInfo ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetAdminTokenInfoRequest {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub search: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GetAdminTokenInfoResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub expired: bool,
|
||||||
|
pub scope: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CreateAdminToken ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateAdminTokenRequest(pub UpdateAdminTokenRequestBody);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreateAdminTokenResponse {
|
||||||
|
pub secret_token: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub info: GetAdminTokenInfoResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UpdateAdminToken ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateAdminTokenRequest {
|
||||||
|
pub id: String,
|
||||||
|
pub body: UpdateAdminTokenRequestBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateAdminTokenRequestBody {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub scope: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateAdminTokenResponse(pub GetAdminTokenInfoResponse);
|
||||||
|
|
||||||
|
// ---- DeleteAdminToken ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeleteAdminTokenRequest {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeleteAdminTokenResponse;
|
||||||
|
|
||||||
// **********************************************
|
// **********************************************
|
||||||
// Layout operations
|
// Layout operations
|
||||||
// **********************************************
|
// **********************************************
|
||||||
|
|
|
@ -21,6 +21,10 @@ pub enum Error {
|
||||||
Common(#[error(source)] CommonError),
|
Common(#[error(source)] CommonError),
|
||||||
|
|
||||||
// Category: cannot process
|
// Category: cannot process
|
||||||
|
/// The admin API token does not exist
|
||||||
|
#[error(display = "Admin token not found: {}", _0)]
|
||||||
|
NoSuchAdminToken(String),
|
||||||
|
|
||||||
/// The API access key does not exist
|
/// The API access key does not exist
|
||||||
#[error(display = "Access key not found: {}", _0)]
|
#[error(display = "Access key not found: {}", _0)]
|
||||||
NoSuchAccessKey(String),
|
NoSuchAccessKey(String),
|
||||||
|
@ -60,6 +64,7 @@ impl Error {
|
||||||
pub fn code(&self) -> &'static str {
|
pub fn code(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Error::Common(c) => c.aws_code(),
|
Error::Common(c) => c.aws_code(),
|
||||||
|
Error::NoSuchAdminToken(_) => "NoSuchAdminToken",
|
||||||
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
||||||
Error::NoSuchWorker(_) => "NoSuchWorker",
|
Error::NoSuchWorker(_) => "NoSuchWorker",
|
||||||
Error::NoSuchBlock(_) => "NoSuchBlock",
|
Error::NoSuchBlock(_) => "NoSuchBlock",
|
||||||
|
@ -73,9 +78,10 @@ impl ApiError for Error {
|
||||||
fn http_status_code(&self) -> StatusCode {
|
fn http_status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Error::Common(c) => c.http_status_code(),
|
Error::Common(c) => c.http_status_code(),
|
||||||
Error::NoSuchAccessKey(_) | Error::NoSuchWorker(_) | Error::NoSuchBlock(_) => {
|
Error::NoSuchAdminToken(_)
|
||||||
StatusCode::NOT_FOUND
|
| Error::NoSuchAccessKey(_)
|
||||||
}
|
| Error::NoSuchWorker(_)
|
||||||
|
| Error::NoSuchBlock(_) => StatusCode::NOT_FOUND,
|
||||||
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,25 @@ impl RequestHandler for GetKeyInfoRequest {
|
||||||
let key = match (self.id, self.search) {
|
let key = match (self.id, self.search) {
|
||||||
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
|
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
|
||||||
(None, Some(search)) => {
|
(None, Some(search)) => {
|
||||||
garage
|
let candidates = garage
|
||||||
.key_helper()
|
.key_table
|
||||||
.get_existing_matching_key(&search)
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||||
|
10,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if candidates.len() != 1 {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"{} matching keys",
|
||||||
|
candidates.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
candidates.into_iter().next().unwrap()
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::bad_request(
|
return Err(Error::bad_request(
|
||||||
|
|
|
@ -11,6 +11,7 @@ mod router_v0;
|
||||||
mod router_v1;
|
mod router_v1;
|
||||||
mod router_v2;
|
mod router_v2;
|
||||||
|
|
||||||
|
mod admin_token;
|
||||||
mod bucket;
|
mod bucket;
|
||||||
mod cluster;
|
mod cluster;
|
||||||
mod key;
|
mod key;
|
||||||
|
|
|
@ -34,6 +34,12 @@ impl AdminApiRequest {
|
||||||
GET GetClusterStatus (),
|
GET GetClusterStatus (),
|
||||||
GET GetClusterHealth (),
|
GET GetClusterHealth (),
|
||||||
POST ConnectClusterNodes (body),
|
POST ConnectClusterNodes (body),
|
||||||
|
// Admin token endpoints
|
||||||
|
GET ListAdminTokens (),
|
||||||
|
GET GetAdminTokenInfo (query_opt::id, query_opt::search),
|
||||||
|
POST CreateAdminToken (body),
|
||||||
|
POST UpdateAdminToken (body_field, query::id),
|
||||||
|
POST DeleteAdminToken (query::id),
|
||||||
// Layout endpoints
|
// Layout endpoints
|
||||||
GET GetClusterLayout (),
|
GET GetClusterLayout (),
|
||||||
GET GetClusterLayoutHistory (),
|
GET GetClusterLayoutHistory (),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use base64::prelude::*;
|
||||||
|
|
||||||
use garage_util::crdt::{self, Crdt};
|
use garage_util::crdt::{self, Crdt};
|
||||||
|
|
||||||
use garage_table::{EmptyKey, Entry, TableSchema};
|
use garage_table::{EmptyKey, Entry, TableSchema};
|
||||||
|
@ -76,7 +78,7 @@ impl AdminApiToken {
|
||||||
};
|
};
|
||||||
|
|
||||||
let prefix = hex::encode(&rand::random::<[u8; 12]>()[..]);
|
let prefix = hex::encode(&rand::random::<[u8; 12]>()[..]);
|
||||||
let secret = hex::encode(&rand::random::<[u8; 32]>()[..]);
|
let secret = BASE64_URL_SAFE_NO_PAD.encode(&rand::random::<[u8; 32]>()[..]);
|
||||||
let token = format!("{}.{}", prefix, secret);
|
let token = format!("{}.{}", prefix, secret);
|
||||||
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
|
@ -3,7 +3,7 @@ use garage_util::error::OkOrMessage;
|
||||||
|
|
||||||
use crate::garage::Garage;
|
use crate::garage::Garage;
|
||||||
use crate::helper::error::*;
|
use crate::helper::error::*;
|
||||||
use crate::key_table::{Key, KeyFilter};
|
use crate::key_table::Key;
|
||||||
|
|
||||||
pub struct KeyHelper<'a>(pub(crate) &'a Garage);
|
pub struct KeyHelper<'a>(pub(crate) &'a Garage);
|
||||||
|
|
||||||
|
@ -33,33 +33,4 @@ impl<'a> KeyHelper<'a> {
|
||||||
.filter(|b| !b.state.is_deleted())
|
.filter(|b| !b.state.is_deleted())
|
||||||
.ok_or_else(|| Error::NoSuchAccessKey(key_id.to_string()))
|
.ok_or_else(|| Error::NoSuchAccessKey(key_id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Key if it is present in key table,
|
|
||||||
/// looking it up by key ID or by a match on its name,
|
|
||||||
/// only if it is in non-deleted state.
|
|
||||||
/// Querying a non-existing key ID or a deleted key
|
|
||||||
/// returns a bad request error.
|
|
||||||
pub async fn get_existing_matching_key(&self, pattern: &str) -> Result<Key, Error> {
|
|
||||||
let candidates = self
|
|
||||||
.0
|
|
||||||
.key_table
|
|
||||||
.get_range(
|
|
||||||
&EmptyKey,
|
|
||||||
None,
|
|
||||||
Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())),
|
|
||||||
10,
|
|
||||||
EnumerationOrder::Forward,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if candidates.len() != 1 {
|
|
||||||
Err(Error::BadRequest(format!(
|
|
||||||
"{} matching keys",
|
|
||||||
candidates.len()
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(candidates.into_iter().next().unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue