mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-05 20:37:41 +00:00
Merge pull request 'creation and expiration dates' (#1010) from key-creation-expiration into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1010
This commit is contained in:
@@ -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"
|
||||
@@ -2280,6 +2272,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"created",
|
||||
"globalAliases",
|
||||
"websiteAccess",
|
||||
"keys",
|
||||
@@ -2297,6 +2290,11 @@
|
||||
"format": "int64",
|
||||
"description": "Total number of bytes used by objects in this bucket"
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Bucket creation date"
|
||||
},
|
||||
"globalAliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -2563,8 +2561,9 @@
|
||||
"GetKeyInfoResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"accessKeyId",
|
||||
"name",
|
||||
"expired",
|
||||
"permissions",
|
||||
"buckets"
|
||||
],
|
||||
@@ -2578,6 +2577,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"
|
||||
},
|
||||
@@ -2873,10 +2889,15 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"created",
|
||||
"globalAliases",
|
||||
"localAliases"
|
||||
],
|
||||
"properties": {
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"globalAliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -2904,9 +2925,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"
|
||||
},
|
||||
@@ -3967,6 +4006,10 @@
|
||||
],
|
||||
"description": "Name of the admin API token"
|
||||
},
|
||||
"neverExpires": {
|
||||
"type": "boolean",
|
||||
"description": "Set the admin token to never expire"
|
||||
},
|
||||
"scope": {
|
||||
"type": [
|
||||
"array",
|
||||
@@ -4068,7 +4111,8 @@
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/KeyPerm"
|
||||
"$ref": "#/components/schemas/KeyPerm",
|
||||
"description": "Permissions to allow for the key"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -4078,15 +4122,29 @@
|
||||
"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"
|
||||
},
|
||||
"neverExpires": {
|
||||
"type": "boolean",
|
||||
"description": "Set the access key to never expire"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@ impl RequestHandler for CreateAdminTokenRequest {
|
||||
AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
|
||||
};
|
||||
|
||||
apply_token_updates(&mut token, self.0);
|
||||
apply_token_updates(&mut token, self.0)?;
|
||||
|
||||
garage.admin_token_table.insert(&token).await?;
|
||||
|
||||
@@ -145,7 +145,7 @@ impl RequestHandler for UpdateAdminTokenRequest {
|
||||
) -> Result<UpdateAdminTokenResponse, Error> {
|
||||
let mut token = get_existing_admin_token(&garage, &self.id).await?;
|
||||
|
||||
apply_token_updates(&mut token, self.body);
|
||||
apply_token_updates(&mut token, self.body)?;
|
||||
|
||||
garage.admin_token_table.insert(&token).await?;
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -208,7 +204,16 @@ async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminA
|
||||
.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
|
||||
}
|
||||
|
||||
fn apply_token_updates(token: &mut AdminApiToken, updates: UpdateAdminTokenRequestBody) {
|
||||
fn apply_token_updates(
|
||||
token: &mut AdminApiToken,
|
||||
updates: UpdateAdminTokenRequestBody,
|
||||
) -> Result<(), Error> {
|
||||
if updates.never_expires && updates.expiration.is_some() {
|
||||
return Err(Error::bad_request(
|
||||
"cannot specify `expiration` and `never_expires`",
|
||||
));
|
||||
}
|
||||
|
||||
let params = token.params_mut().unwrap();
|
||||
|
||||
if let Some(name) = updates.name {
|
||||
@@ -219,7 +224,12 @@ fn apply_token_updates(token: &mut AdminApiToken, updates: UpdateAdminTokenReque
|
||||
.expiration
|
||||
.update(Some(expiration.timestamp_millis() as u64));
|
||||
}
|
||||
if updates.never_expires {
|
||||
params.expiration.update(None);
|
||||
}
|
||||
if let Some(scope) = updates.scope {
|
||||
params.scope.update(AdminApiTokenScope(scope));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+27
-8
@@ -3,6 +3,7 @@ use std::convert::TryFrom;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use paste::paste;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
@@ -321,11 +322,11 @@ pub struct GetAdminTokenInfoResponse {
|
||||
/// Identifier of the admin token (which is also a prefix of the full bearer token)
|
||||
pub id: Option<String>,
|
||||
/// Creation date
|
||||
pub created: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created: Option<DateTime<Utc>>,
|
||||
/// Name of the admin API token
|
||||
pub name: String,
|
||||
/// Expiration time and date, formatted according to RFC 3339
|
||||
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub expiration: Option<DateTime<Utc>>,
|
||||
/// Whether this admin token is expired already
|
||||
pub expired: bool,
|
||||
/// Scope of the admin API token, a list of admin endpoint names (such as
|
||||
@@ -364,7 +365,10 @@ pub struct UpdateAdminTokenRequestBody {
|
||||
/// Name of the admin API token
|
||||
pub name: Option<String>,
|
||||
/// Expiration time and date, formatted according to RFC 3339
|
||||
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub expiration: Option<DateTime<Utc>>,
|
||||
/// Set the admin token to never expire
|
||||
#[serde(default)]
|
||||
pub never_expires: bool,
|
||||
/// Scope of the admin API token, a list of admin endpoint names (such as
|
||||
/// `GetClusterStatus`, etc), or the special value `*` to allow all
|
||||
/// admin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or
|
||||
@@ -636,6 +640,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 ----
|
||||
@@ -655,8 +662,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,
|
||||
@@ -694,9 +704,7 @@ pub struct ApiBucketKeyPerm {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateKeyRequest {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
pub struct CreateKeyRequest(pub UpdateKeyRequestBody);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateKeyResponse(pub GetKeyInfoResponse);
|
||||
@@ -728,8 +736,16 @@ 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<String>,
|
||||
/// Expiration time and date, formatted according to RFC 3339
|
||||
pub expiration: Option<DateTime<Utc>>,
|
||||
/// Set the access key to never expire
|
||||
#[serde(default)]
|
||||
pub never_expires: bool,
|
||||
/// Permissions to allow for the key
|
||||
pub allow: Option<KeyPerm>,
|
||||
/// Permissions to deny for the key
|
||||
pub deny: Option<KeyPerm>,
|
||||
}
|
||||
|
||||
@@ -759,6 +775,7 @@ pub struct ListBucketsResponse(pub Vec<ListBucketsResponseItem>);
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListBucketsResponseItem {
|
||||
pub id: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub global_aliases: Vec<String>,
|
||||
pub local_aliases: Vec<BucketLocalAlias>,
|
||||
}
|
||||
@@ -788,6 +805,8 @@ pub struct GetBucketInfoRequest {
|
||||
pub struct GetBucketInfoResponse {
|
||||
/// Identifier of the bucket
|
||||
pub id: String,
|
||||
/// Bucket creation date
|
||||
pub created: DateTime<Utc>,
|
||||
/// List of global aliases for this bucket
|
||||
pub global_aliases: Vec<String>,
|
||||
/// Whether website acces is enabled for this bucket
|
||||
@@ -932,7 +951,7 @@ pub struct InspectObjectVersion {
|
||||
/// Version ID
|
||||
pub uuid: String,
|
||||
/// Creation timestamp of this object version
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Whether this object version was created with SSE-C encryption
|
||||
pub encrypted: bool,
|
||||
/// Whether this object version is still uploading
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -48,6 +48,8 @@ impl RequestHandler for ListBucketsRequest {
|
||||
let state = b.state.as_option().unwrap();
|
||||
ListBucketsResponseItem {
|
||||
id: hex::encode(b.id),
|
||||
created: DateTime::from_timestamp_millis(state.creation_date as i64)
|
||||
.expect("invalid timestamp stored in db"),
|
||||
global_aliases: state
|
||||
.aliases
|
||||
.items()
|
||||
@@ -677,6 +679,8 @@ async fn bucket_info_results(
|
||||
let quotas = state.quotas.get();
|
||||
let res = GetBucketInfoResponse {
|
||||
id: hex::encode(bucket.id),
|
||||
created: DateTime::from_timestamp_millis(state.creation_date as i64)
|
||||
.expect("invalid timestamp stored in db"),
|
||||
global_aliases: state
|
||||
.aliases
|
||||
.items()
|
||||
|
||||
+67
-19
@@ -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<_>>();
|
||||
|
||||
@@ -85,7 +103,10 @@ impl RequestHandler for CreateKeyRequest {
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<CreateKeyResponse, Error> {
|
||||
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(
|
||||
@@ -131,21 +152,7 @@ impl RequestHandler for UpdateKeyRequest {
|
||||
) -> Result<UpdateKeyResponse, Error> {
|
||||
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?;
|
||||
|
||||
@@ -205,6 +212,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())
|
||||
@@ -250,3 +264,37 @@ async fn key_info_results(
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) -> Result<(), Error> {
|
||||
if updates.never_expires && updates.expiration.is_some() {
|
||||
return Err(Error::bad_request(
|
||||
"cannot specify `expiration` and `never_expires`",
|
||||
));
|
||||
}
|
||||
|
||||
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 updates.never_expires {
|
||||
key_state.expiration.update(None);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use format_table::format_table;
|
||||
|
||||
use chrono::{Local, Utc};
|
||||
use chrono::Local;
|
||||
|
||||
use garage_util::error::*;
|
||||
|
||||
@@ -78,16 +78,11 @@ impl Cli {
|
||||
}
|
||||
|
||||
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),
|
||||
expiration: parse_expires_in(&opt.expires_in)?,
|
||||
never_expires: false,
|
||||
scope: opt.scope.map(|s| {
|
||||
s.split(",")
|
||||
.map(|x| x.trim().to_string())
|
||||
@@ -121,6 +116,7 @@ impl Cli {
|
||||
body: UpdateAdminTokenRequestBody {
|
||||
name: Some(new),
|
||||
expiration: None,
|
||||
never_expires: false,
|
||||
scope: None,
|
||||
},
|
||||
})
|
||||
@@ -144,12 +140,8 @@ impl Cli {
|
||||
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),
|
||||
expiration: parse_expires_in(&opt.expires_in)?,
|
||||
never_expires: opt.never_expires,
|
||||
scope: opt.scope.map({
|
||||
let mut new_scope = token.scope;
|
||||
|scope_str| {
|
||||
@@ -231,9 +223,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{}",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//use bytesize::ByteSize;
|
||||
use format_table::format_table;
|
||||
|
||||
use chrono::Local;
|
||||
|
||||
use garage_util::error::*;
|
||||
|
||||
use garage_api_admin::api::*;
|
||||
@@ -29,13 +31,16 @@ impl Cli {
|
||||
}
|
||||
|
||||
pub async fn cmd_list_buckets(&self) -> Result<(), Error> {
|
||||
let buckets = self.api_request(ListBucketsRequest).await?;
|
||||
let mut buckets = self.api_request(ListBucketsRequest).await?;
|
||||
|
||||
let mut table = vec!["ID\tGlobal aliases\tLocal aliases".to_string()];
|
||||
buckets.0.sort_by_key(|x| x.created);
|
||||
|
||||
let mut table = vec!["ID\tCreated\tGlobal aliases\tLocal aliases".to_string()];
|
||||
for bucket in buckets.0.iter() {
|
||||
table.push(format!(
|
||||
"{:.16}\t{}\t{}",
|
||||
"{:.16}\t{}\t{}\t{}",
|
||||
bucket.id,
|
||||
bucket.created.with_timezone(&Local).date_naive(),
|
||||
table_list_abbr(&bucket.global_aliases),
|
||||
table_list_abbr(
|
||||
bucket
|
||||
@@ -484,6 +489,7 @@ fn print_bucket_info(bucket: &GetBucketInfoResponse) {
|
||||
|
||||
let mut info = vec![
|
||||
format!("Bucket:\t{}", bucket.id),
|
||||
format!("Created:\t{}", bucket.created.with_timezone(&Local)),
|
||||
String::new(),
|
||||
{
|
||||
let size = bytesize::ByteSize::b(bucket.bytes as u64);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use format_table::format_table;
|
||||
|
||||
use chrono::Local;
|
||||
|
||||
use garage_util::error::*;
|
||||
|
||||
use garage_api_admin::api::*;
|
||||
@@ -14,19 +16,38 @@ 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,
|
||||
KeyOperation::Import(query) => self.cmd_import_key(query).await,
|
||||
KeyOperation::DeleteExpired { yes } => self.cmd_delete_expired_keys(yes).await,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -49,9 +70,13 @@ 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: parse_expires_in(&opt.expires_in)?,
|
||||
never_expires: false,
|
||||
allow: None,
|
||||
deny: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
print_key_info(&key.0);
|
||||
@@ -73,6 +98,35 @@ impl Cli {
|
||||
id: key.access_key_id,
|
||||
body: UpdateKeyRequestBody {
|
||||
name: Some(opt.new_name),
|
||||
expiration: None,
|
||||
never_expires: false,
|
||||
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: parse_expires_in(&opt.expires_in)?,
|
||||
never_expires: opt.never_expires,
|
||||
allow: None,
|
||||
deny: None,
|
||||
},
|
||||
@@ -124,6 +178,8 @@ impl Cli {
|
||||
id: key.access_key_id,
|
||||
body: UpdateKeyRequestBody {
|
||||
name: None,
|
||||
expiration: None,
|
||||
never_expires: false,
|
||||
allow: Some(KeyPerm {
|
||||
create_bucket: opt.create_bucket,
|
||||
}),
|
||||
@@ -151,6 +207,8 @@ impl Cli {
|
||||
id: key.access_key_id,
|
||||
body: UpdateKeyRequestBody {
|
||||
name: None,
|
||||
expiration: None,
|
||||
never_expires: false,
|
||||
allow: None,
|
||||
deny: Some(KeyPerm {
|
||||
create_bucket: opt.create_bucket,
|
||||
@@ -181,20 +239,62 @@ impl Cli {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cmd_delete_expired_keys(&self, yes: bool) -> Result<(), Error> {
|
||||
let mut list = self.api_request(ListKeysRequest).await?.0;
|
||||
|
||||
list.retain(|key| key.expired);
|
||||
|
||||
if !yes {
|
||||
return Err(Error::Message(format!(
|
||||
"This would delete {} access keys, add the --yes flag to proceed.",
|
||||
list.len(),
|
||||
)));
|
||||
}
|
||||
|
||||
for key in list.iter() {
|
||||
let id = key.id.clone();
|
||||
println!("Deleting access key `{}` ({})", key.name, id);
|
||||
self.api_request(DeleteKeyRequest { id }).await?;
|
||||
}
|
||||
|
||||
println!("{} access keys have been deleted.", list.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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 ====");
|
||||
|
||||
@@ -12,6 +12,8 @@ use std::convert::TryFrom;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use garage_util::error::*;
|
||||
|
||||
use garage_rpc::*;
|
||||
@@ -162,3 +164,11 @@ pub fn table_list_abbr<T: IntoIterator<Item = S>, S: AsRef<str>>(values: T) -> S
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_expires_in(expires_in: &Option<String>) -> Result<Option<DateTime<Utc>>, Error> {
|
||||
expires_in
|
||||
.as_ref()
|
||||
.map(|x| parse_duration::parse::parse(&x).map(|dur| Utc::now() + dur))
|
||||
.transpose()
|
||||
.ok_or_message("Invalid duration passed for --expires-in parameter")
|
||||
}
|
||||
|
||||
@@ -426,6 +426,18 @@ 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),
|
||||
|
||||
/// Delete all expired access keys
|
||||
#[structopt(name = "delete-expired", version = garage_version())]
|
||||
DeleteExpired {
|
||||
/// Confirm deletion
|
||||
#[structopt(long = "yes")]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
@@ -442,6 +454,24 @@ 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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
/// Set the access key to never expire
|
||||
#[structopt(long = "never-expires")]
|
||||
pub never_expires: bool,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
@@ -568,10 +598,15 @@ pub struct AdminTokenCreateOp {
|
||||
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 the token to never expire
|
||||
#[structopt(long = "never-expires")]
|
||||
pub never_expires: bool,
|
||||
|
||||
/// Set a limited scope for the token, as a comma-separated list of
|
||||
/// admin API functions (e.g. GetClusterStatus, etc.), or `*` to allow
|
||||
/// all admin API functions.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user