From 88b4623bf14f597cc19fb69d2f82e36e8046ca40 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 12 Mar 2025 09:52:39 +0100 Subject: [PATCH] add creation date to admin api tokens --- doc/api/garage-admin-v2.json | 8 ++++++++ src/api/admin/admin_token.rs | 6 ++++++ src/api/admin/api.rs | 2 ++ src/garage/cli/remote/admin_token.rs | 24 ++++++++++++++++-------- src/model/admin_token_table.rs | 5 +++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index 8f3517cb..91d92e11 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -2048,6 +2048,14 @@ "scope" ], "properties": { + "created": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Creation date" + }, "expiration": { "type": [ "string", diff --git a/src/api/admin/admin_token.rs b/src/api/admin/admin_token.rs index aca7a519..04bfdd96 100644 --- a/src/api/admin/admin_token.rs +++ b/src/api/admin/admin_token.rs @@ -41,6 +41,7 @@ impl RequestHandler for ListAdminTokensRequest { 0, GetAdminTokenInfoResponse { id: None, + created: None, name: "admin_token (from daemon configuration)".into(), expiration: None, expired: false, @@ -54,6 +55,7 @@ impl RequestHandler for ListAdminTokensRequest { 1, GetAdminTokenInfoResponse { id: None, + created: None, name: "metrics_token (from daemon configuration)".into(), expiration: None, expired: false, @@ -180,6 +182,10 @@ fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInf GetAdminTokenInfoResponse { id: Some(token.prefix.clone()), + created: Some( + DateTime::from_timestamp_millis(params.created as i64) + .expect("invalid timestamp stored in db"), + ), 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") diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 11ffb772..fde304f4 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -314,6 +314,8 @@ pub struct GetAdminTokenInfoRequest { pub struct GetAdminTokenInfoResponse { /// Identifier of the admin token (which is also a prefix of the full bearer token) pub id: Option, + /// Creation date + pub created: Option>, /// Name of the admin API token pub name: String, /// Expiration time and date, formatted according to RFC 3339 diff --git a/src/garage/cli/remote/admin_token.rs b/src/garage/cli/remote/admin_token.rs index 464480a1..4d765b92 100644 --- a/src/garage/cli/remote/admin_token.rs +++ b/src/garage/cli/remote/admin_token.rs @@ -1,6 +1,6 @@ use format_table::format_table; -use chrono::Utc; +use chrono::{Local, Utc}; use garage_util::error::*; @@ -30,11 +30,15 @@ impl Cli { } pub async fn cmd_list_admin_tokens(&self) -> Result<(), Error> { - let list = self.api_request(ListAdminTokensRequest).await?; + let mut list = self.api_request(ListAdminTokensRequest).await?; - let mut table = vec!["ID\tNAME\tEXPIRATION\tSCOPE".to_string()]; + list.0.sort_by_key(|x| x.created); + + let mut table = vec!["ID\tCREATED\tNAME\tEXPIRATION\tSCOPE".to_string()]; for tok in list.0.iter() { - let scope = if tok.scope.len() > 1 { + let scope = if tok.expired { + String::new() + } else if tok.scope.len() > 1 { format!("[{}]", tok.scope.len()) } else { tok.scope.get(0).cloned().unwrap_or_default() @@ -43,12 +47,15 @@ impl Cli { "expired".to_string() } else { tok.expiration - .map(|x| x.to_string()) + .map(|x| x.with_timezone(&Local).to_string()) .unwrap_or("never".into()) }; table.push(format!( - "{}\t{}\t{}\t{}\t", + "{}\t{}\t{}\t{}\t{}", tok.id.as_deref().unwrap_or("-"), + tok.created + .map(|x| x.with_timezone(&Local).date_naive().to_string()) + .unwrap_or("-".into()), tok.name, exp, scope, @@ -209,8 +216,9 @@ impl Cli { fn print_token_info(token: &GetAdminTokenInfoResponse) { format_table(vec![ - format!("ID:\t{}", token.id.as_deref().unwrap_or("-")), + format!("ID:\t{}", token.id.as_ref().unwrap()), format!("Name:\t{}", token.name), + format!("Created:\t{}", token.created.unwrap().with_timezone(&Local)), format!( "Validity:\t{}", token.expired.then_some("EXPIRED").unwrap_or("valid") @@ -219,7 +227,7 @@ fn print_token_info(token: &GetAdminTokenInfoResponse) { "Expiration:\t{}", token .expiration - .map(|x| x.to_string()) + .map(|x| x.with_timezone(&Local).to_string()) .unwrap_or("never".into()) ), format!("Scope:\t{}", token.scope.to_vec().join(", ")), diff --git a/src/model/admin_token_table.rs b/src/model/admin_token_table.rs index f3940299..ef91eb4a 100644 --- a/src/model/admin_token_table.rs +++ b/src/model/admin_token_table.rs @@ -1,6 +1,7 @@ use base64::prelude::*; use garage_util::crdt::{self, Crdt}; +use garage_util::time::now_msec; use garage_table::{EmptyKey, Entry, TableSchema}; @@ -24,6 +25,9 @@ mod v2 { #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct AdminApiTokenParams { + /// Creation date + pub created: u64, + /// The entire API token hashed as a password pub token_hash: String, @@ -91,6 +95,7 @@ impl AdminApiToken { let ret = AdminApiToken { prefix, state: crdt::Deletable::present(AdminApiTokenParams { + created: now_msec(), token_hash: hashed_token, name: crdt::Lww::new(name.to_string()), expiration: crdt::Lww::new(None),