admin and cli: hide secret keys unless asked
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
Alex 2023-06-14 16:56:15 +02:00
parent 4a82f6380e
commit 7895f99d3a
7 changed files with 58 additions and 22 deletions

View file

@ -411,6 +411,9 @@ If `id` is set, the key is looked up using its exact identifier (faster).
If `search` is set, the key is looked up using its name or prefix If `search` is set, the key is looked up using its name or prefix
of identifier (slower, all keys are enumerated to do this). of identifier (slower, all keys are enumerated to do this).
Optionnally, the query parameter `showSecretKey=true` can be set to reveal the
associated secret access key.
Example response: Example response:
```json ```json

View file

@ -242,8 +242,13 @@ impl ApiHandler for AdminApiServer {
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await, Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await,
// Keys // Keys
Endpoint::ListKeys => handle_list_keys(&self.garage).await, Endpoint::ListKeys => handle_list_keys(&self.garage).await,
Endpoint::GetKeyInfo { id, search } => { Endpoint::GetKeyInfo {
handle_get_key_info(&self.garage, id, search).await id,
search,
show_secret_key,
} => {
let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
handle_get_key_info(&self.garage, id, search, show_secret_key).await
} }
Endpoint::CreateKey => handle_create_key(&self.garage, req).await, Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
Endpoint::ImportKey => handle_import_key(&self.garage, req).await, Endpoint::ImportKey => handle_import_key(&self.garage, req).await,

View file

@ -10,7 +10,7 @@ use garage_model::garage::Garage;
use garage_model::key_table::*; use garage_model::key_table::*;
use crate::admin::error::*; use crate::admin::error::*;
use crate::helpers::{json_ok_response, parse_json_body}; use crate::helpers::{is_default, json_ok_response, parse_json_body};
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Error> { pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
let res = garage let res = garage
@ -44,6 +44,7 @@ pub async fn handle_get_key_info(
garage: &Arc<Garage>, garage: &Arc<Garage>,
id: Option<String>, id: Option<String>,
search: Option<String>, search: Option<String>,
show_secret_key: bool,
) -> Result<Response<Body>, Error> { ) -> Result<Response<Body>, Error> {
let key = if let Some(id) = id { let key = if let Some(id) = id {
garage.key_helper().get_existing_key(&id).await? garage.key_helper().get_existing_key(&id).await?
@ -56,7 +57,7 @@ pub async fn handle_get_key_info(
unreachable!(); unreachable!();
}; };
key_info_results(garage, key).await key_info_results(garage, key, show_secret_key).await
} }
pub async fn handle_create_key( pub async fn handle_create_key(
@ -68,7 +69,7 @@ pub async fn handle_create_key(
let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key")); let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key"));
garage.key_table.insert(&key).await?; garage.key_table.insert(&key).await?;
key_info_results(garage, key).await key_info_results(garage, key, true).await
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -88,10 +89,14 @@ pub async fn handle_import_key(
return Err(Error::KeyAlreadyExists(req.access_key_id.to_string())); return Err(Error::KeyAlreadyExists(req.access_key_id.to_string()));
} }
let imported_key = Key::import(&req.access_key_id, &req.secret_access_key, &req.name); let imported_key = Key::import(
&req.access_key_id,
&req.secret_access_key,
req.name.as_deref().unwrap_or("Imported key"),
);
garage.key_table.insert(&imported_key).await?; garage.key_table.insert(&imported_key).await?;
key_info_results(garage, imported_key).await key_info_results(garage, imported_key, false).await
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -99,7 +104,7 @@ pub async fn handle_import_key(
struct ImportKeyRequest { struct ImportKeyRequest {
access_key_id: String, access_key_id: String,
secret_access_key: String, secret_access_key: String,
name: String, name: Option<String>,
} }
pub async fn handle_update_key( pub async fn handle_update_key(
@ -129,7 +134,7 @@ pub async fn handle_update_key(
garage.key_table.insert(&key).await?; garage.key_table.insert(&key).await?;
key_info_results(garage, key).await key_info_results(garage, key, false).await
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -152,7 +157,11 @@ pub async fn handle_delete_key(garage: &Arc<Garage>, id: String) -> Result<Respo
.body(Body::empty())?) .body(Body::empty())?)
} }
async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Body>, Error> { async fn key_info_results(
garage: &Arc<Garage>,
key: Key,
show_secret: bool,
) -> Result<Response<Body>, Error> {
let mut relevant_buckets = HashMap::new(); let mut relevant_buckets = HashMap::new();
let key_state = key.state.as_option().unwrap(); let key_state = key.state.as_option().unwrap();
@ -181,7 +190,11 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
let res = GetKeyInfoResult { let res = GetKeyInfoResult {
name: key_state.name.get().clone(), name: key_state.name.get().clone(),
access_key_id: key.key_id.clone(), access_key_id: key.key_id.clone(),
secret_access_key: key_state.secret_key.clone(), secret_access_key: if show_secret {
Some(key_state.secret_key.clone())
} else {
None
},
permissions: KeyPerm { permissions: KeyPerm {
create_bucket: *key_state.allow_create_bucket.get(), create_bucket: *key_state.allow_create_bucket.get(),
}, },
@ -227,7 +240,8 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
struct GetKeyInfoResult { struct GetKeyInfoResult {
name: String, name: String,
access_key_id: String, access_key_id: String,
secret_access_key: String, #[serde(skip_serializing_if = "is_default")]
secret_access_key: Option<String>,
permissions: KeyPerm, permissions: KeyPerm,
buckets: Vec<KeyInfoBucketResult>, buckets: Vec<KeyInfoBucketResult>,
} }

View file

@ -35,6 +35,7 @@ pub enum Endpoint {
GetKeyInfo { GetKeyInfo {
id: Option<String>, id: Option<String>,
search: Option<String>, search: Option<String>,
show_secret_key: Option<String>,
}, },
DeleteKey { DeleteKey {
id: String, id: String,
@ -104,11 +105,11 @@ impl Endpoint {
POST ("/v0/layout/apply" | "/v1/layout/apply") => ApplyClusterLayout, POST ("/v0/layout/apply" | "/v1/layout/apply") => ApplyClusterLayout,
POST ("/v0/layout/revert" | "/v1/layout/revert") => RevertClusterLayout, POST ("/v0/layout/revert" | "/v1/layout/revert") => RevertClusterLayout,
// API key endpoints // API key endpoints
GET ("/v0/key" | "/v1/key") if id => GetKeyInfo (query_opt::id, query_opt::search), GET "/v1/key" if id => GetKeyInfo (query_opt::id, query_opt::search, query_opt::show_secret_key),
GET ("/v0/key" | "/v1/key") if search => GetKeyInfo (query_opt::id, query_opt::search), GET "/v1/key" if search => GetKeyInfo (query_opt::id, query_opt::search, query_opt::show_secret_key),
POST ("/v0/key" | "/v1/key") if id => UpdateKey (query::id), POST "/v1/key" if id => UpdateKey (query::id),
POST ("/v0/key" | "/v1/key") => CreateKey, POST "/v1/key" => CreateKey,
POST ("/v0/key/import" | "/v1/key/import") => ImportKey, POST "/v1/key/import" => ImportKey,
DELETE ("/v0/key" | "/v1/key") if id => DeleteKey (query::id), DELETE ("/v0/key" | "/v1/key") if id => DeleteKey (query::id),
GET ("/v0/key" | "/v1/key") => ListKeys, GET ("/v0/key" | "/v1/key") => ListKeys,
// Bucket endpoints // Bucket endpoints
@ -153,6 +154,7 @@ generateQueryParameters! {
"search" => search, "search" => search,
"globalAlias" => global_alias, "globalAlias" => global_alias,
"alias" => alias, "alias" => alias,
"accessKeyId" => access_key_id "accessKeyId" => access_key_id,
"showSecretKey" => show_secret_key
] ]
} }

View file

@ -152,6 +152,10 @@ pub fn json_ok_response<T: Serialize>(res: &T) -> Result<Response<Body>, Error>
.body(Body::from(resp_json))?) .body(Body::from(resp_json))?)
} }
pub fn is_default<T: Default + PartialEq>(v: &T) -> bool {
*v == T::default()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -41,12 +41,17 @@ impl AdminRpcHandler {
Ok(AdminRpc::KeyList(key_ids)) Ok(AdminRpc::KeyList(key_ids))
} }
async fn handle_key_info(&self, query: &KeyOpt) -> Result<AdminRpc, Error> { async fn handle_key_info(&self, query: &KeyInfoOpt) -> Result<AdminRpc, Error> {
let key = self let mut key = self
.garage .garage
.key_helper() .key_helper()
.get_existing_matching_key(&query.key_pattern) .get_existing_matching_key(&query.key_pattern)
.await?; .await?;
if !query.show_secret {
key.state.as_option_mut().unwrap().secret_key = "(redacted)".into();
}
self.key_info_result(key).await self.key_info_result(key).await
} }

View file

@ -328,7 +328,7 @@ pub enum KeyOperation {
/// Get key info /// Get key info
#[structopt(name = "info", version = garage_version())] #[structopt(name = "info", version = garage_version())]
Info(KeyOpt), Info(KeyInfoOpt),
/// Create new key /// Create new key
#[structopt(name = "create", version = garage_version())] #[structopt(name = "create", version = garage_version())]
@ -356,9 +356,12 @@ pub enum KeyOperation {
} }
#[derive(Serialize, Deserialize, StructOpt, Debug)] #[derive(Serialize, Deserialize, StructOpt, Debug)]
pub struct KeyOpt { pub struct KeyInfoOpt {
/// ID or name of the key /// ID or name of the key
pub key_pattern: String, pub key_pattern: String,
/// Whether to display the secret key
#[structopt(long = "show-secret")]
pub show_secret: bool,
} }
#[derive(Serialize, Deserialize, StructOpt, Debug)] #[derive(Serialize, Deserialize, StructOpt, Debug)]