mirror of
https://git.deuxfleurs.fr/Deuxfleurs/garage.git
synced 2025-03-29 13:15:27 +00:00
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",
|
||||
"async-trait",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"err-derive",
|
||||
"format_table",
|
||||
"futures",
|
||||
|
|
|
@ -48,7 +48,7 @@ blake2 = "0.10"
|
|||
bytes = "1.0"
|
||||
bytesize = "1.1"
|
||||
cfg-if = "1.0"
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
crc32fast = "1.4"
|
||||
crc32c = "0.6"
|
||||
crypto-common = "0.1"
|
||||
|
|
|
@ -25,6 +25,7 @@ garage_api_common.workspace = true
|
|||
argon2.workspace = true
|
||||
async-trait.workspace = true
|
||||
bytesize.workspace = true
|
||||
chrono.workspace = true
|
||||
err-derive.workspace = true
|
||||
hex.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,
|
||||
ConnectClusterNodes,
|
||||
|
||||
// Admin tokens operations
|
||||
ListAdminTokens,
|
||||
GetAdminTokenInfo,
|
||||
CreateAdminToken,
|
||||
UpdateAdminToken,
|
||||
DeleteAdminToken,
|
||||
|
||||
// Layout operations
|
||||
GetClusterLayout,
|
||||
GetClusterLayoutHistory,
|
||||
|
@ -282,6 +289,84 @@ pub struct ConnectNodeResponse {
|
|||
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
|
||||
// **********************************************
|
||||
|
|
|
@ -21,6 +21,10 @@ pub enum Error {
|
|||
Common(#[error(source)] CommonError),
|
||||
|
||||
// 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
|
||||
#[error(display = "Access key not found: {}", _0)]
|
||||
NoSuchAccessKey(String),
|
||||
|
@ -60,6 +64,7 @@ impl Error {
|
|||
pub fn code(&self) -> &'static str {
|
||||
match self {
|
||||
Error::Common(c) => c.aws_code(),
|
||||
Error::NoSuchAdminToken(_) => "NoSuchAdminToken",
|
||||
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
||||
Error::NoSuchWorker(_) => "NoSuchWorker",
|
||||
Error::NoSuchBlock(_) => "NoSuchBlock",
|
||||
|
@ -73,9 +78,10 @@ impl ApiError for Error {
|
|||
fn http_status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::Common(c) => c.http_status_code(),
|
||||
Error::NoSuchAccessKey(_) | Error::NoSuchWorker(_) | Error::NoSuchBlock(_) => {
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
Error::NoSuchAdminToken(_)
|
||||
| Error::NoSuchAccessKey(_)
|
||||
| Error::NoSuchWorker(_)
|
||||
| Error::NoSuchBlock(_) => StatusCode::NOT_FOUND,
|
||||
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,10 +46,25 @@ impl RequestHandler for GetKeyInfoRequest {
|
|||
let key = match (self.id, self.search) {
|
||||
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
|
||||
(None, Some(search)) => {
|
||||
garage
|
||||
.key_helper()
|
||||
.get_existing_matching_key(&search)
|
||||
let candidates = garage
|
||||
.key_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 keys",
|
||||
candidates.len()
|
||||
)));
|
||||
}
|
||||
candidates.into_iter().next().unwrap()
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::bad_request(
|
||||
|
|
|
@ -11,6 +11,7 @@ mod router_v0;
|
|||
mod router_v1;
|
||||
mod router_v2;
|
||||
|
||||
mod admin_token;
|
||||
mod bucket;
|
||||
mod cluster;
|
||||
mod key;
|
||||
|
|
|
@ -34,6 +34,12 @@ impl AdminApiRequest {
|
|||
GET GetClusterStatus (),
|
||||
GET GetClusterHealth (),
|
||||
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
|
||||
GET GetClusterLayout (),
|
||||
GET GetClusterLayoutHistory (),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use base64::prelude::*;
|
||||
|
||||
use garage_util::crdt::{self, Crdt};
|
||||
|
||||
use garage_table::{EmptyKey, Entry, TableSchema};
|
||||
|
@ -76,7 +78,7 @@ impl AdminApiToken {
|
|||
};
|
||||
|
||||
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 salt = SaltString::generate(&mut OsRng);
|
||||
|
|
|
@ -3,7 +3,7 @@ use garage_util::error::OkOrMessage;
|
|||
|
||||
use crate::garage::Garage;
|
||||
use crate::helper::error::*;
|
||||
use crate::key_table::{Key, KeyFilter};
|
||||
use crate::key_table::Key;
|
||||
|
||||
pub struct KeyHelper<'a>(pub(crate) &'a Garage);
|
||||
|
||||
|
@ -33,33 +33,4 @@ impl<'a> KeyHelper<'a> {
|
|||
.filter(|b| !b.state.is_deleted())
|
||||
.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…
Reference in a new issue