cli: add functions to manage admin api tokens

This commit is contained in:
Alex Auvolat 2025-03-11 18:09:24 +01:00
parent ec0da3b644
commit 1bd7689301
4 changed files with 356 additions and 0 deletions

View file

@ -38,6 +38,7 @@ garage_web.workspace = true
backtrace.workspace = true
bytes.workspace = true
bytesize.workspace = true
chrono.workspace = true
timeago.workspace = true
parse_duration.workspace = true
hex.workspace = true

View file

@ -0,0 +1,227 @@
use format_table::format_table;
use chrono::Utc;
use garage_util::error::*;
use garage_api_admin::api::*;
use crate::cli::remote::*;
use crate::cli::structs::*;
impl Cli {
pub async fn cmd_admin_token(&self, cmd: AdminTokenOperation) -> Result<(), Error> {
match cmd {
AdminTokenOperation::List => self.cmd_list_admin_tokens().await,
AdminTokenOperation::Info { api_token } => self.cmd_admin_token_info(api_token).await,
AdminTokenOperation::Create(opt) => self.cmd_create_admin_token(opt).await,
AdminTokenOperation::Rename {
api_token,
new_name,
} => self.cmd_rename_admin_token(api_token, new_name).await,
AdminTokenOperation::Set(opt) => self.cmd_update_admin_token(opt).await,
AdminTokenOperation::Delete { api_token, yes } => {
self.cmd_delete_admin_token(api_token, yes).await
}
AdminTokenOperation::DeleteExpired { yes } => {
self.cmd_delete_expired_admin_tokens(yes).await
}
}
}
pub async fn cmd_list_admin_tokens(&self) -> Result<(), Error> {
let list = self.api_request(ListAdminTokensRequest).await?;
let mut table = vec!["ID\tNAME\tEXPIRATION\tSCOPE".to_string()];
for tok in list.0.iter() {
let scope = if tok.scope.len() > 1 {
format!("[{}]", tok.scope.len())
} else {
tok.scope.get(0).cloned().unwrap_or_default()
};
let exp = if tok.expired {
"expired".to_string()
} else {
tok.expiration
.map(|x| x.to_string())
.unwrap_or("never".into())
};
table.push(format!(
"{}\t{}\t{}\t{}\t",
tok.id.as_deref().unwrap_or("-"),
tok.name,
exp,
scope,
));
}
format_table(table);
Ok(())
}
pub async fn cmd_admin_token_info(&self, search: String) -> Result<(), Error> {
let info = self
.api_request(GetAdminTokenInfoRequest {
id: None,
search: Some(search),
})
.await?;
print_token_info(&info);
Ok(())
}
pub async fn cmd_create_admin_token(&self, opt: AdminTokenCreateOp) -> Result<(), Error> {
// TODO
let res = self
.api_request(CreateAdminTokenRequest(UpdateAdminTokenRequestBody {
name: opt.name,
expiration: opt
.expires_in
.map(|x| parse_duration::parse::parse(&x))
.transpose()
.ok_or_message("Invalid duration passed for --expires-in parameter")?
.map(|dur| Utc::now() + dur),
scope: opt.scope.map(|s| {
s.split(",")
.map(|x| x.trim().to_string())
.collect::<Vec<_>>()
}),
}))
.await?;
if opt.quiet {
println!("{}", res.secret_token);
} else {
println!("This is your secret bearer token, it will not be shown again by Garage:");
println!("\n {}\n", res.secret_token);
print_token_info(&res.info);
}
Ok(())
}
pub async fn cmd_rename_admin_token(&self, old: String, new: String) -> Result<(), Error> {
let token = self
.api_request(GetAdminTokenInfoRequest {
id: None,
search: Some(old),
})
.await?;
let info = self
.api_request(UpdateAdminTokenRequest {
id: token.id.unwrap(),
body: UpdateAdminTokenRequestBody {
name: Some(new),
expiration: None,
scope: None,
},
})
.await?;
print_token_info(&info.0);
Ok(())
}
pub async fn cmd_update_admin_token(&self, opt: AdminTokenSetOp) -> Result<(), Error> {
let token = self
.api_request(GetAdminTokenInfoRequest {
id: None,
search: Some(opt.api_token),
})
.await?;
let info = self
.api_request(UpdateAdminTokenRequest {
id: token.id.unwrap(),
body: UpdateAdminTokenRequestBody {
name: None,
expiration: opt
.expires_in
.map(|x| parse_duration::parse::parse(&x))
.transpose()
.ok_or_message("Invalid duration passed for --expires-in parameter")?
.map(|dur| Utc::now() + dur),
scope: opt.scope.map(|s| {
s.split(",")
.map(|x| x.trim().to_string())
.collect::<Vec<_>>()
}),
},
})
.await?;
print_token_info(&info.0);
Ok(())
}
pub async fn cmd_delete_admin_token(&self, token: String, yes: bool) -> Result<(), Error> {
let token = self
.api_request(GetAdminTokenInfoRequest {
id: None,
search: Some(token),
})
.await?;
let id = token.id.unwrap();
if !yes {
return Err(Error::Message(format!(
"Add the --yes flag to delete API token `{}` ({})",
token.name, id
)));
}
self.api_request(DeleteAdminTokenRequest { id }).await?;
println!("Admin API token has been deleted.");
Ok(())
}
pub async fn cmd_delete_expired_admin_tokens(&self, yes: bool) -> Result<(), Error> {
let mut list = self.api_request(ListAdminTokensRequest).await?.0;
list.retain(|tok| tok.expired);
if !yes {
return Err(Error::Message(format!(
"This would delete {} admin API tokens, add the --yes flag to proceed.",
list.len(),
)));
}
for token in list.iter() {
let id = token.id.clone().unwrap();
println!("Deleting token `{}` ({})", token.name, id);
self.api_request(DeleteAdminTokenRequest { id }).await?;
}
println!("{} admin API tokens have been deleted.", list.len());
Ok(())
}
}
fn print_token_info(token: &GetAdminTokenInfoResponse) {
format_table(vec![
format!("ID:\t{}", token.id.as_deref().unwrap_or("-")),
format!("Name:\t{}", token.name),
format!(
"Validity:\t{}",
token.expired.then_some("EXPIRED").unwrap_or("valid")
),
format!(
"Expiration:\t{}",
token
.expiration
.map(|x| x.to_string())
.unwrap_or("never".into())
),
format!("Scope:\t{}", token.scope.to_vec().join(", ")),
]);
}

View file

@ -1,3 +1,4 @@
pub mod admin_token;
pub mod bucket;
pub mod cluster;
pub mod key;
@ -35,6 +36,7 @@ impl Cli {
}
Command::Layout(layout_opt) => self.layout_command_dispatch(layout_opt).await,
Command::Bucket(bo) => self.cmd_bucket(bo).await,
Command::AdminToken(to) => self.cmd_admin_token(to).await,
Command::Key(ko) => self.cmd_key(ko).await,
Command::Worker(wo) => self.cmd_worker(wo).await,
Command::Block(bo) => self.cmd_block(bo).await,

View file

@ -30,6 +30,10 @@ pub enum Command {
#[structopt(name = "key", version = garage_version())]
Key(KeyOperation),
/// Operations on admin API tokens
#[structopt(name = "admin-token", version = garage_version())]
AdminToken(AdminTokenOperation),
/// Start repair of node data on remote node
#[structopt(name = "repair", version = garage_version())]
Repair(RepairOpt),
@ -64,6 +68,10 @@ pub enum Command {
AdminApiSchema,
}
// -------------------------
// ---- garage node ... ----
// -------------------------
#[derive(StructOpt, Debug)]
pub enum NodeOperation {
/// Print the full node ID (public key) of this Garage node, and its publicly reachable IP
@ -91,6 +99,10 @@ pub struct ConnectNodeOpt {
pub(crate) node: String,
}
// ---------------------------
// ---- garage layout ... ----
// ---------------------------
#[derive(StructOpt, Debug)]
pub enum LayoutOperation {
/// Assign role to Garage node
@ -193,6 +205,10 @@ pub struct SkipDeadNodesOpt {
pub(crate) allow_missing_data: bool,
}
// ---------------------------
// ---- garage bucket ... ----
// ---------------------------
#[derive(StructOpt, Debug)]
pub enum BucketOperation {
/// List buckets
@ -350,6 +366,10 @@ pub struct CleanupIncompleteUploadsOpt {
pub buckets: Vec<String>,
}
// ------------------------
// ---- garage key ... ----
// ------------------------
#[derive(StructOpt, Debug)]
pub enum KeyOperation {
/// List keys
@ -447,6 +467,92 @@ pub struct KeyImportOpt {
pub yes: bool,
}
// --------------------------------
// ---- garage admin-token ... ----
// --------------------------------
#[derive(StructOpt, Debug)]
pub enum AdminTokenOperation {
/// List all admin API tokens
#[structopt(name = "list", version = garage_version())]
List,
/// Fetch info about a specific admin API token
#[structopt(name = "info", version = garage_version())]
Info {
/// Name or prefix of the ID of the token to look up
api_token: String,
},
/// Create new admin API token
#[structopt(name = "create", version = garage_version())]
Create(AdminTokenCreateOp),
/// Rename an admin API token
#[structopt(name = "rename", version = garage_version())]
Rename {
/// Name or prefix of the ID of the token to rename
api_token: String,
/// New name of the admintoken
new_name: String,
},
/// Set parameters for an admin API token
#[structopt(name = "set", version = garage_version())]
Set(AdminTokenSetOp),
/// Delete an admin API token
#[structopt(name = "delete", version = garage_version())]
Delete {
/// Name or prefix of the ID of the token to delete
api_token: String,
/// Confirm deletion
#[structopt(long = "yes")]
yes: bool,
},
/// Delete all expired admin API tokens
#[structopt(name = "delete-expired", version = garage_version())]
DeleteExpired {
/// Confirm deletion
#[structopt(long = "yes")]
yes: bool,
},
}
#[derive(StructOpt, Debug, Clone)]
pub struct AdminTokenCreateOp {
/// Set a name for the token
pub name: Option<String>,
/// Set an expiration time for the token (see docs.rs/parse_duration for date
/// format)
#[structopt(long = "expires-in")]
pub expires_in: Option<String>,
/// Set a limited scope for the token (by default, `*`)
#[structopt(long = "scope")]
pub scope: Option<String>,
/// Print only the newly generated API token to stdout
#[structopt(short = "q", long = "quiet")]
pub quiet: bool,
}
#[derive(StructOpt, Debug, Clone)]
pub struct AdminTokenSetOp {
/// Name or prefix of the ID of the token to modify
pub api_token: String,
/// Set an expiration time for the token (see docs.rs/parse_duration for date
/// format)
#[structopt(long = "expires-in")]
pub expires_in: Option<String>,
/// Set a limited scope for the token
#[structopt(long = "scope")]
pub scope: Option<String>,
}
// ---------------------------
// ---- garage repair ... ----
// ---------------------------
#[derive(StructOpt, Debug, Clone)]
pub struct RepairOpt {
/// Launch repair operation on all nodes
@ -508,6 +614,10 @@ pub enum ScrubCmd {
Cancel,
}
// -----------------------------------
// ---- garage offline-repair ... ----
// -----------------------------------
#[derive(StructOpt, Debug, Clone)]
pub struct OfflineRepairOpt {
/// Confirm the launch of the repair operation
@ -529,6 +639,10 @@ pub enum OfflineRepairWhat {
ObjectCounters,
}
// --------------------------
// ---- garage stats ... ----
// --------------------------
#[derive(StructOpt, Debug, Clone)]
pub struct StatsOpt {
/// Gather statistics from all nodes
@ -536,6 +650,10 @@ pub struct StatsOpt {
pub all_nodes: bool,
}
// ---------------------------
// ---- garage worker ... ----
// ---------------------------
#[derive(StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum WorkerOperation {
/// List all workers on Garage node
@ -579,6 +697,10 @@ pub struct WorkerListOpt {
pub errors: bool,
}
// --------------------------
// ---- garage block ... ----
// --------------------------
#[derive(StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum BlockOperation {
/// List all blocks that currently have a resync error
@ -611,6 +733,10 @@ pub enum BlockOperation {
},
}
// -------------------------
// ---- garage meta ... ----
// -------------------------
#[derive(StructOpt, Debug, Eq, PartialEq, Clone, Copy)]
pub enum MetaOperation {
/// Save a snapshot of the metadata db file