From 590c9bb4db16c77bf3b558e58fbe03c58f87f938 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 17 Apr 2025 11:30:58 +0200 Subject: [PATCH] possibility to update access key expiration date --- doc/api/garage-admin-v2.json | 27 +++++++++++--------- src/api/admin/api.rs | 9 ++++--- src/api/admin/key.rs | 44 ++++++++++++++++++++------------ src/garage/cli/remote/key.rs | 49 +++++++++++++++++++++++++++++++++--- src/garage/cli/structs.rs | 19 ++++++++++++++ 5 files changed, 114 insertions(+), 34 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index 4cdcf708..4cc907d1 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -2162,15 +2162,7 @@ "$ref": "#/components/schemas/GetBucketInfoResponse" }, "CreateKeyRequest": { - "type": "object", - "properties": { - "name": { - "type": [ - "string", - "null" - ] - } - } + "$ref": "#/components/schemas/UpdateKeyRequestBody" }, "CreateKeyResponse": { "$ref": "#/components/schemas/GetKeyInfoResponse" @@ -4115,7 +4107,8 @@ "type": "null" }, { - "$ref": "#/components/schemas/KeyPerm" + "$ref": "#/components/schemas/KeyPerm", + "description": "Permissions to allow for the key" } ] }, @@ -4125,15 +4118,25 @@ "type": "null" }, { - "$ref": "#/components/schemas/KeyPerm" + "$ref": "#/components/schemas/KeyPerm", + "description": "Permissions to deny for the key" } ] }, + "expiration": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Expiration time and date, formatted according to RFC 3339" + }, "name": { "type": [ "string", "null" - ] + ], + "description": "Name of the API key" } } }, diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 4c0cfa45..fa6c6b2d 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -701,9 +701,7 @@ pub struct ApiBucketKeyPerm { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct CreateKeyRequest { - pub name: Option, -} +pub struct CreateKeyRequest(pub UpdateKeyRequestBody); #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct CreateKeyResponse(pub GetKeyInfoResponse); @@ -735,8 +733,13 @@ pub struct UpdateKeyResponse(pub GetKeyInfoResponse); #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateKeyRequestBody { + /// Name of the API key pub name: Option, + /// Expiration time and date, formatted according to RFC 3339 + pub expiration: Option>, + /// Permissions to allow for the key pub allow: Option, + /// Permissions to deny for the key pub deny: Option, } diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs index ee3a4d1c..07373e76 100644 --- a/src/api/admin/key.rs +++ b/src/api/admin/key.rs @@ -103,7 +103,10 @@ impl RequestHandler for CreateKeyRequest { garage: &Arc, _admin: &Admin, ) -> Result { - let key = Key::new(self.name.as_deref().unwrap_or("Unnamed key")); + let mut key = Key::new("Unnamed key"); + + apply_key_updates(&mut key, self.0); + garage.key_table.insert(&key).await?; Ok(CreateKeyResponse( @@ -149,21 +152,7 @@ impl RequestHandler for UpdateKeyRequest { ) -> Result { let mut key = garage.key_helper().get_existing_key(&self.id).await?; - let key_state = key.state.as_option_mut().unwrap(); - - if let Some(new_name) = self.body.name { - key_state.name.update(new_name); - } - if let Some(allow) = self.body.allow { - if allow.create_bucket { - key_state.allow_create_bucket.update(true); - } - } - if let Some(deny) = self.body.deny { - if deny.create_bucket { - key_state.allow_create_bucket.update(false); - } - } + apply_key_updates(&mut key, self.body); garage.key_table.insert(&key).await?; @@ -275,3 +264,26 @@ async fn key_info_results( Ok(res) } + +fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) { + let key_state = key.state.as_option_mut().unwrap(); + + if let Some(new_name) = updates.name { + key_state.name.update(new_name); + } + if let Some(expiration) = updates.expiration { + key_state + .expiration + .update(Some(expiration.timestamp_millis() as u64)); + } + if let Some(allow) = updates.allow { + if allow.create_bucket { + key_state.allow_create_bucket.update(true); + } + } + if let Some(deny) = updates.deny { + if deny.create_bucket { + key_state.allow_create_bucket.update(false); + } + } +} diff --git a/src/garage/cli/remote/key.rs b/src/garage/cli/remote/key.rs index 25937efa..d254f4e0 100644 --- a/src/garage/cli/remote/key.rs +++ b/src/garage/cli/remote/key.rs @@ -1,6 +1,6 @@ use format_table::format_table; -use chrono::Local; +use chrono::{Local, Utc}; use garage_util::error::*; @@ -16,6 +16,7 @@ impl Cli { KeyOperation::Info(query) => self.cmd_key_info(query).await, KeyOperation::Create(query) => self.cmd_create_key(query).await, KeyOperation::Rename(query) => self.cmd_rename_key(query).await, + KeyOperation::Set(opt) => self.cmd_update_key(opt).await, KeyOperation::Delete(query) => self.cmd_delete_key(query).await, KeyOperation::Allow(query) => self.cmd_allow_key(query).await, KeyOperation::Deny(query) => self.cmd_deny_key(query).await, @@ -68,9 +69,17 @@ impl Cli { pub async fn cmd_create_key(&self, opt: KeyNewOpt) -> Result<(), Error> { let key = self - .api_request(CreateKeyRequest { + .api_request(CreateKeyRequest(UpdateKeyRequestBody { name: Some(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), + allow: None, + deny: None, + })) .await?; print_key_info(&key.0); @@ -92,6 +101,38 @@ impl Cli { id: key.access_key_id, body: UpdateKeyRequestBody { name: Some(opt.new_name), + expiration: None, + allow: None, + deny: None, + }, + }) + .await?; + + print_key_info(&new_key.0); + + Ok(()) + } + + pub async fn cmd_update_key(&self, opt: KeySetOpt) -> Result<(), Error> { + let key = self + .api_request(GetKeyInfoRequest { + id: None, + search: Some(opt.key_pattern), + show_secret_key: false, + }) + .await?; + + let new_key = self + .api_request(UpdateKeyRequest { + id: key.access_key_id, + body: UpdateKeyRequestBody { + 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), allow: None, deny: None, }, @@ -143,6 +184,7 @@ impl Cli { id: key.access_key_id, body: UpdateKeyRequestBody { name: None, + expiration: None, allow: Some(KeyPerm { create_bucket: opt.create_bucket, }), @@ -170,6 +212,7 @@ impl Cli { id: key.access_key_id, body: UpdateKeyRequestBody { name: None, + expiration: None, allow: None, deny: Some(KeyPerm { create_bucket: opt.create_bucket, diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 20079709..01a5d77f 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -426,6 +426,10 @@ pub enum KeyOperation { /// Import key #[structopt(name = "import", version = garage_version())] Import(KeyImportOpt), + + /// Set parameters for an access key + #[structopt(name = "set", version = garage_version())] + Set(KeySetOpt), } #[derive(StructOpt, Debug)] @@ -442,6 +446,21 @@ pub struct KeyNewOpt { /// Name of the key #[structopt(default_value = "Unnamed key")] pub name: String, + /// Set an expiration time for the access key + /// (see docs.rs/parse_duration for date format) + #[structopt(long = "expires-in")] + pub expires_in: Option, +} + +#[derive(StructOpt, Debug)] +pub struct KeySetOpt { + /// ID or name of the key + pub key_pattern: String, + + /// Set an expiration time for the access key + /// (see docs.rs/parse_duration for date format) + #[structopt(long = "expires-in")] + pub expires_in: Option, } #[derive(StructOpt, Debug)]