use crate::commands::tenants::admin_request;
use clap::{Subcommand, ValueEnum};
use serde_json::{json, Value};
#[derive(Subcommand)]
pub enum AdminAction {
Secrets {
#[command(subcommand)]
action: SecretsAction,
},
}
#[derive(Subcommand)]
pub enum SecretsAction {
Reseal {
#[arg(long, value_enum)]
operation: Operation,
#[arg(long)]
tenant: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "snake_case")]
pub enum Operation {
Encrypt,
RotateKek,
RotateDek,
}
impl Operation {
fn as_str(self) -> &'static str {
match self {
Operation::Encrypt => "encrypt",
Operation::RotateKek => "rotate_kek",
Operation::RotateDek => "rotate_dek",
}
}
}
fn reseal_body(operation: Operation, tenant: Option<&str>) -> Value {
json!({ "operation": operation.as_str(), "tenant": tenant })
}
pub async fn run(
action: AdminAction,
api_url: Option<&str>,
env: Option<&str>,
) -> eyre::Result<()> {
match action {
AdminAction::Secrets {
action: SecretsAction::Reseal { operation, tenant },
} => reseal(operation, tenant.as_deref(), api_url, env).await,
}
}
async fn reseal(
operation: Operation,
tenant: Option<&str>,
api_url: Option<&str>,
env: Option<&str>,
) -> eyre::Result<()> {
let report = admin_request(
"Reseal",
reqwest::Method::POST,
"/api/admin/secrets/reseal",
Some(reseal_body(operation, tenant)),
api_url,
env,
)
.await?;
println!(
"Resealed secrets ({}, {}): {} rows checked, {} rewritten, {} data keys rewrapped, {} tenants rotated",
operation.as_str(),
tenant.unwrap_or("every tenant"),
report["rows_checked"],
report["rows_rewritten"],
report["keys_rewrapped"],
report["tenants_rotated"],
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct Admin {
#[command(subcommand)]
action: AdminAction,
}
fn parsed(args: &[&str]) -> Result<(Operation, Option<String>), clap::Error> {
Admin::try_parse_from(["admin"].iter().chain(args)).map(|admin| match admin.action {
AdminAction::Secrets {
action: SecretsAction::Reseal { operation, tenant },
} => (operation, tenant),
})
}
#[test]
fn an_operation_is_one_of_three_and_goes_to_the_platform_by_its_name() {
for (given, operation) in [
("encrypt", Operation::Encrypt),
("rotate_kek", Operation::RotateKek),
("rotate_dek", Operation::RotateDek),
] {
let (parsed, tenant) =
parsed(&["secrets", "reseal", "--operation", given]).expect(given);
assert_eq!((parsed, tenant), (operation, None));
assert_eq!(
reseal_body(parsed, None),
json!({ "operation": given, "tenant": null })
);
}
assert_eq!(
parsed(&[
"secrets",
"reseal",
"--operation",
"encrypt",
"--tenant",
"acme"
])
.expect("a tenant"),
(Operation::Encrypt, Some("acme".into()))
);
}
#[test]
fn another_operation_is_refused_before_anything_is_asked() {
let refused = parsed(&["secrets", "reseal", "--operation", "rotate"])
.expect_err("an unknown operation");
assert_eq!(refused.kind(), clap::error::ErrorKind::InvalidValue);
assert!(refused.to_string().contains("rotate_kek"), "{refused}");
assert!(
parsed(&["secrets", "reseal"]).is_err(),
"the operation is required"
);
}
}