add creation date and expiration date to access keys

This commit is contained in:
Alex Auvolat
2025-04-17 11:09:21 +02:00
parent 2f21181ccb
commit c56b7e20c3
10 changed files with 225 additions and 33 deletions
+38 -2
View File
@@ -2569,8 +2569,9 @@
"GetKeyInfoResponse": {
"type": "object",
"required": [
"name",
"accessKeyId",
"name",
"expired",
"permissions",
"buckets"
],
@@ -2584,6 +2585,23 @@
"$ref": "#/components/schemas/KeyInfoBucketResponse"
}
},
"created": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiration": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expired": {
"type": "boolean"
},
"name": {
"type": "string"
},
@@ -2915,9 +2933,27 @@
"type": "object",
"required": [
"id",
"name"
"name",
"expired"
],
"properties": {
"created": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiration": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expired": {
"type": "boolean"
},
"id": {
"type": "string"
},
+1 -5
View File
@@ -190,11 +190,7 @@ fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInf
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),
expired: params.is_expired(now),
scope: params.scope.get().0.clone(),
}
}
+7 -1
View File
@@ -637,6 +637,9 @@ pub struct ListKeysResponse(pub Vec<ListKeysResponseItem>);
pub struct ListKeysResponseItem {
pub id: String,
pub name: String,
pub created: Option<DateTime<Utc>>,
pub expiration: Option<DateTime<Utc>>,
pub expired: bool,
}
// ---- GetKeyInfo ----
@@ -656,8 +659,11 @@ pub struct GetKeyInfoRequest {
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetKeyInfoResponse {
pub name: String,
pub access_key_id: String,
pub created: Option<DateTime<Utc>>,
pub name: String,
pub expiration: Option<DateTime<Utc>>,
pub expired: bool,
#[serde(default, skip_serializing_if = "is_default")]
pub secret_access_key: Option<String>,
pub permissions: KeyPerm,
+2 -13
View File
@@ -272,19 +272,8 @@ fn verify_authorization(
.admin_token_table
.get_local(&EmptyKey, &prefix.to_string())?
.and_then(|k| k.state.into_option())
.filter(|p| {
p.expiration
.get()
.map(|exp| now_msec() < exp)
.unwrap_or(true)
})
.filter(|p| {
p.scope
.get()
.0
.iter()
.any(|x| x == "*" || x == endpoint_name)
})
.filter(|p| !p.is_expired(now_msec()))
.filter(|p| p.has_scope(endpoint_name))
.ok_or_else(|| Error::forbidden(invalid_msg))?
.token_hash
} else {
+28 -3
View File
@@ -1,7 +1,10 @@
use std::collections::HashMap;
use std::sync::Arc;
use chrono::DateTime;
use garage_table::*;
use garage_util::time::now_msec;
use garage_model::garage::Garage;
use garage_model::key_table::*;
@@ -14,6 +17,8 @@ impl RequestHandler for ListKeysRequest {
type Response = ListKeysResponse;
async fn handle(self, garage: &Arc<Garage>, _admin: &Admin) -> Result<ListKeysResponse, Error> {
let now = now_msec();
let res = garage
.key_table
.get_range(
@@ -25,9 +30,22 @@ impl RequestHandler for ListKeysRequest {
)
.await?
.iter()
.map(|k| ListKeysResponseItem {
id: k.key_id.to_string(),
name: k.params().unwrap().name.get().clone(),
.map(|k| {
let p = k.params().unwrap();
ListKeysResponseItem {
id: k.key_id.to_string(),
name: p.name.get().clone(),
created: p.created.map(|x| {
DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db")
}),
expiration: p.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db")
}),
expired: p.is_expired(now),
}
})
.collect::<Vec<_>>();
@@ -205,6 +223,13 @@ async fn key_info_results(
let res = GetKeyInfoResponse {
name: key_state.name.get().clone(),
created: key_state.created.map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}),
expiration: key_state.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}),
expired: key_state.is_expired(now_msec()),
access_key_id: key.key_id.clone(),
secret_access_key: if show_secret {
Some(key_state.secret_key.clone())
+8
View File
@@ -9,6 +9,7 @@ use sha2::{Digest, Sha256};
use garage_table::*;
use garage_util::data::Hash;
use garage_util::time::now_msec;
use garage_model::garage::Garage;
use garage_model::key_table::*;
@@ -396,6 +397,13 @@ pub fn verify_v4(
.ok_or_else(|| Error::forbidden(format!("No such key: {}", &auth.key_id)))?;
let key_p = key.params().unwrap();
if key_p.is_expired(now_msec()) {
return Err(Error::forbidden(format!(
"Access key {} has expired",
key.key_id
)));
}
let mut hmac = signing_hmac(
&auth.date,
&key_p.secret_key,
+3 -2
View File
@@ -231,9 +231,10 @@ impl Cli {
}
fn print_token_info(token: &GetAdminTokenInfoResponse) {
println!("==== ADMINISTRATION TOKEN INFORMATION ====");
let mut table = vec![
format!("ID:\t{}", token.id.as_ref().unwrap()),
format!("Name:\t{}", token.name),
format!("Token ID:\t{}", token.id.as_ref().unwrap()),
format!("Token name:\t{}", token.name),
format!("Created:\t{}", token.created.unwrap().with_timezone(&Local)),
format!(
"Validity:\t{}",
+43 -5
View File
@@ -1,5 +1,7 @@
use format_table::format_table;
use chrono::Local;
use garage_util::error::*;
use garage_api_admin::api::*;
@@ -22,11 +24,28 @@ impl Cli {
}
pub async fn cmd_list_keys(&self) -> Result<(), Error> {
let keys = self.api_request(ListKeysRequest).await?;
let mut keys = self.api_request(ListKeysRequest).await?;
let mut table = vec!["ID\tName".to_string()];
keys.0.sort_by_key(|x| x.created);
let mut table = vec!["ID\tCreated\tName\tExpiration".to_string()];
for key in keys.0.iter() {
table.push(format!("{}\t{}", key.id, key.name));
let exp = if key.expired {
"expired".to_string()
} else {
key.expiration
.map(|x| x.with_timezone(&Local).to_string())
.unwrap_or("never".into())
};
table.push(format!(
"{}\t{}\t{}\t{}",
key.id,
key.created
.map(|x| x.with_timezone(&Local).date_naive().to_string())
.unwrap_or_default(),
key.name,
exp
));
}
format_table(table);
@@ -186,15 +205,34 @@ impl Cli {
fn print_key_info(key: &GetKeyInfoResponse) {
println!("==== ACCESS KEY INFORMATION ====");
format_table(vec![
format!("Key name:\t{}", key.name),
let mut table = vec![
format!("Key ID:\t{}", key.access_key_id),
format!("Key name:\t{}", key.name),
format!(
"Secret key:\t{}",
key.secret_access_key.as_deref().unwrap_or("(redacted)")
),
];
if let Some(c) = key.created {
table.push(format!("Created:\t{}", c.with_timezone(&Local)));
}
table.extend([
format!(
"Validity:\t{}",
key.expired.then_some("EXPIRED").unwrap_or("valid")
),
format!(
"Expiration:\t{}",
key.expiration
.map(|x| x.with_timezone(&Local).to_string())
.unwrap_or("never".into())
),
String::new(),
format!("Can create buckets:\t{}", key.permissions.create_bucket),
]);
format_table(table);
println!("");
println!("==== BUCKETS FOR THIS KEY ====");
+14 -1
View File
@@ -113,7 +113,7 @@ impl AdminApiToken {
}
}
/// Returns true if this represents a deleted bucket
/// Returns true if this represents a deleted admin token
pub fn is_deleted(&self) -> bool {
self.state.is_deleted()
}
@@ -137,6 +137,19 @@ impl AdminApiToken {
}
}
impl AdminApiTokenParams {
pub fn is_expired(&self, ts_now: u64) -> bool {
match *self.expiration.get() {
None => false,
Some(exp) => ts_now >= exp,
}
}
pub fn has_scope(&self, endpoint: &str) -> bool {
self.scope.get().0.iter().any(|x| x == "*" || x == endpoint)
}
}
impl Entry<EmptyKey, String> for AdminApiToken {
fn partition_key(&self) -> &EmptyKey {
&EmptyKey
+81 -1
View File
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
use garage_util::crdt::{self, Crdt};
use garage_util::data::*;
use garage_util::time::now_msec;
use garage_table::{DeletedFilter, EmptyKey, Entry, TableSchema};
@@ -48,13 +49,82 @@ mod v08 {
impl garage_util::migrate::InitialFormat for Key {}
}
pub use v08::*;
mod v2 {
use crate::permission::BucketKeyPerm;
use garage_util::crdt;
use garage_util::data::Uuid;
use serde::{Deserialize, Serialize};
use super::v08;
/// An api key
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct Key {
/// The id of the key (immutable), used as partition key
pub key_id: String,
/// Internal state of the key
pub state: crdt::Deletable<KeyParams>,
}
/// Configuration for a key
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct KeyParams {
/// Key's creation date, if known (older versions of Garage didn't keep track
/// of this information)
pub created: Option<u64>,
/// The secret_key associated (immutable)
pub secret_key: String,
/// Name for the key
pub name: crdt::Lww<String>,
/// The optional time of expiration of the key
pub expiration: crdt::Lww<Option<u64>>,
/// Flag to allow users having this key to create buckets
pub allow_create_bucket: crdt::Lww<bool>,
/// If the key is present: it gives some permissions,
/// a map of bucket IDs (uuids) to permissions.
/// Otherwise no permissions are granted to key
pub authorized_buckets: crdt::Map<Uuid, BucketKeyPerm>,
/// A key can have a local view of buckets names it is
/// the only one to see, this is the namespace for these aliases
pub local_aliases: crdt::LwwMap<String, Option<Uuid>>,
}
impl garage_util::migrate::Migrate for Key {
const VERSION_MARKER: &'static [u8] = b"G2key";
type Previous = v08::Key;
fn migrate(old: v08::Key) -> Key {
Key {
key_id: old.key_id,
state: old.state.map(|x| KeyParams {
created: None,
secret_key: x.secret_key,
name: x.name,
expiration: crdt::Lww::raw(0, None),
allow_create_bucket: x.allow_create_bucket,
authorized_buckets: x.authorized_buckets,
local_aliases: x.local_aliases,
}),
}
}
}
}
pub use v2::*;
impl KeyParams {
fn new(secret_key: &str, name: &str) -> Self {
KeyParams {
created: Some(now_msec()),
secret_key: secret_key.to_string(),
name: crdt::Lww::new(name.to_string()),
expiration: crdt::Lww::new(None),
allow_create_bucket: crdt::Lww::new(false),
authorized_buckets: crdt::Map::new(),
local_aliases: crdt::LwwMap::new(),
@@ -65,6 +135,7 @@ impl KeyParams {
impl Crdt for KeyParams {
fn merge(&mut self, o: &Self) {
self.name.merge(&o.name);
self.expiration.merge(&o.expiration);
self.allow_create_bucket.merge(&o.allow_create_bucket);
self.authorized_buckets.merge(&o.authorized_buckets);
self.local_aliases.merge(&o.local_aliases);
@@ -145,6 +216,15 @@ impl Key {
}
}
impl KeyParams {
pub fn is_expired(&self, ts_now: u64) -> bool {
match *self.expiration.get() {
None => false,
Some(exp) => ts_now >= exp,
}
}
}
impl Entry<EmptyKey, String> for Key {
fn partition_key(&self) -> &EmptyKey {
&EmptyKey