From 1bd7689301c843119b6f0c34851729e89b768803 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 11 Mar 2025 18:09:24 +0100 Subject: [PATCH] cli: add functions to manage admin api tokens --- src/garage/Cargo.toml | 1 + src/garage/cli/remote/admin_token.rs | 227 +++++++++++++++++++++++++++ src/garage/cli/remote/mod.rs | 2 + src/garage/cli/structs.rs | 126 +++++++++++++++ 4 files changed, 356 insertions(+) create mode 100644 src/garage/cli/remote/admin_token.rs diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index ba747fdf..045a6174 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -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 diff --git a/src/garage/cli/remote/admin_token.rs b/src/garage/cli/remote/admin_token.rs new file mode 100644 index 00000000..464480a1 --- /dev/null +++ b/src/garage/cli/remote/admin_token.rs @@ -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::>() + }), + })) + .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::>() + }), + }, + }) + .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(", ")), + ]); +} diff --git a/src/garage/cli/remote/mod.rs b/src/garage/cli/remote/mod.rs index 40673b91..237b6db9 100644 --- a/src/garage/cli/remote/mod.rs +++ b/src/garage/cli/remote/mod.rs @@ -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, diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 0af92c35..0b0a8b94 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -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, } +// ------------------------ +// ---- 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, + /// Set an expiration time for the token (see docs.rs/parse_duration for date + /// format) + #[structopt(long = "expires-in")] + pub expires_in: Option, + /// Set a limited scope for the token (by default, `*`) + #[structopt(long = "scope")] + pub scope: Option, + /// 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, + /// Set a limited scope for the token + #[structopt(long = "scope")] + pub scope: Option, +} + +// --------------------------- +// ---- 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