use super::{elapsed, VAULT_ENV_NAME};
use crate::{
commands::{
key::{CurveName, KeySize, KeyType},
map_tags, map_vec, AttributeArgs, IsDefault, OutputFormat,
},
credential, TableExt,
};
use akv_cli::{json, parsing::parse_key_value_opt, Error, Result};
use azure_core::{http::Url, time::OffsetDateTime, Bytes};
use azure_security_keyvault_certificates::{
models::{
Certificate, CertificateAttributes, CertificateClientGetCertificateOptions,
CertificateClientUpdateCertificatePropertiesOptions, CertificatePolicy,
CertificateProperties, CreateCertificateParameters, IssuerParameters, KeyProperties,
UpdateCertificatePropertiesParameters, X509CertificateProperties,
},
CertificateClient, ResourceExt as _, ResourceId,
};
use clap::{ArgAction, ArgGroup, Subcommand, ValueEnum};
use futures::TryStreamExt as _;
use indicatif::ProgressBar;
use prettytable::{color, format, Attr, Cell, Row, Table};
use std::{fmt, time::Duration};
use timeago::Formatter;
use tracing::{Level, Span};
#[derive(Debug, Subcommand)]
pub enum Commands {
Create {
#[arg(long)]
name: String,
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Url,
#[arg(long, default_value = "Self")]
issuer: String,
#[arg(long, default_value = "CN=DefaultPolicy")]
subject: String,
#[arg(long, default_value_t = 3)]
validity: u32,
#[arg(id = "type", long, value_enum)]
r#type: KeyType,
#[arg(long, value_parser, required_if_eq("type", "rsa"))]
size: Option<KeySize>,
#[arg(long, value_enum, required_if_eq("type", "ec"))]
curve: Option<CurveName>,
#[arg(long, action = ArgAction::SetTrue, default_value_t = false)]
exportable: bool,
#[arg(long, action = ArgAction::SetTrue, default_value_t = false)]
reuse_key: bool,
#[arg(long, value_enum, value_delimiter = ',')]
key_usage: Vec<KeyUsageType>,
#[arg(long, value_delimiter = ',')]
enhanced_key_usage: Vec<String>,
#[command(flatten)]
attributes: AttributeArgs,
#[arg(long, value_name = "NAME[=VALUE]", value_parser = parse_key_value_opt::<String>)]
tags: Vec<(String, Option<String>)>,
#[arg(short = 'o', long, value_enum, default_value_t)]
output: OutputFormat,
},
#[command(group(ArgGroup::new("ident").args(&["id", "name"]).required(true)))]
Edit {
#[arg(value_name = "URL")]
id: Option<Url>,
#[arg(long, requires = "vault")]
name: Option<String>,
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Option<Url>,
#[command(flatten)]
attributes: AttributeArgs,
#[arg(long, value_name = "NAME[=VALUE]", value_parser = parse_key_value_opt::<String>)]
tags: Vec<(String, Option<String>)>,
#[arg(short = 'o', long, value_enum, default_value_t)]
output: OutputFormat,
},
#[command(group(ArgGroup::new("ident").args(&["id", "name"]).required(true)))]
EditPolicy {
#[arg(value_name = "URL")]
id: Option<Url>,
#[arg(long, requires = "vault")]
name: Option<String>,
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Option<Url>,
#[arg(long)]
issuer: Option<String>,
#[arg(long)]
subject: Option<String>,
#[arg(long)]
validity: Option<u32>,
#[arg(id = "type", long, value_enum)]
r#type: Option<KeyType>,
#[arg(long, value_parser, required_if_eq("type", "rsa"))]
size: Option<KeySize>,
#[arg(long, value_enum, required_if_eq("type", "ec"))]
curve: Option<CurveName>,
#[arg(long, action = ArgAction::SetTrue)]
exportable: Option<bool>,
#[arg(long, action = ArgAction::SetTrue)]
reuse_key: Option<bool>,
#[arg(long, value_enum, value_delimiter = ',')]
key_usage: Option<Vec<KeyUsageType>>,
#[arg(long, value_delimiter = ',')]
enhanced_key_usage: Option<Vec<String>>,
},
#[command(group(ArgGroup::new("ident").args(&["id", "name"]).required(true)))]
Get {
#[arg(value_name = "URL")]
id: Option<Url>,
#[arg(long, requires = "vault")]
name: Option<String>,
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Option<Url>,
#[arg(short = 'o', long, value_enum, default_value_t)]
output: OutputFormat,
},
#[command(group(ArgGroup::new("ident").args(&["id", "name"]).required(true)))]
GetPolicy {
#[arg(value_name = "URL")]
id: Option<Url>,
#[arg(long, requires = "vault")]
name: Option<String>,
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Option<Url>,
},
List {
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Url,
#[arg(long)]
long: bool,
#[arg(short = 'o', long, value_enum, default_value_t)]
output: OutputFormat,
},
#[command(group(ArgGroup::new("ident").args(&["id", "name"]).required(true)))]
ListVersions {
#[arg(value_name = "URL")]
id: Option<Url>,
#[arg(long, requires = "vault")]
name: Option<String>,
#[arg(long, value_name = "URL", env = VAULT_ENV_NAME)]
vault: Option<Url>,
#[arg(long)]
long: bool,
#[arg(short = 'o', long, value_enum, default_value_t)]
output: OutputFormat,
},
}
impl Commands {
pub async fn handle(&self, global_args: &crate::Args) -> Result<()> {
match &self {
Commands::Create { .. } => self.create(global_args).await,
Commands::Edit { .. } => self.edit(global_args).await,
Commands::EditPolicy { .. } => self.edit_policy(global_args).await,
Commands::Get { .. } => self.get(global_args).await,
Commands::GetPolicy { .. } => self.get_policy(global_args).await,
Commands::List { .. } => self.list(global_args).await,
Commands::ListVersions { .. } => self.list_versions(global_args).await,
}
}
#[tracing::instrument(level = Level::INFO, skip(self, global_args), fields(vault, name), err)]
async fn create(&self, global_args: &crate::Args) -> Result<()> {
let Commands::Create {
name,
vault,
issuer,
subject,
validity,
r#type,
size,
curve,
exportable,
reuse_key,
key_usage,
enhanced_key_usage,
attributes:
AttributeArgs {
enabled,
expires,
not_before,
},
tags,
output,
} = self
else {
panic!("invalid command");
};
let current = Span::current();
current.record("vault", vault.as_str());
current.record("name", name);
let client = CertificateClient::new(vault.as_str(), credential()?, None)?;
let certificate_attributes = CertificateAttributes {
enabled: Some(*enabled),
expires: *expires,
not_before: *not_before,
..Default::default()
};
let params = CreateCertificateParameters {
certificate_policy: Some(CertificatePolicy {
key_properties: Some(KeyProperties {
key_type: Some(r#type.into()),
key_size: size.map(|value| *value),
curve: curve.map(Into::into),
exportable: Some(*exportable),
reuse_key: Some(*reuse_key),
}),
issuer_parameters: Some(IssuerParameters {
name: Some(issuer.clone()),
..Default::default()
}),
x509_certificate_properties: Some(X509CertificateProperties {
key_usage: map_vec(Some(key_usage), Into::into),
enhanced_key_usage: map_vec(Some(enhanced_key_usage), Clone::clone),
subject: Some(subject.clone()),
validity_in_months: Some(*validity as i32),
..Default::default()
}),
..Default::default()
}),
tags: map_tags(tags),
certificate_attributes: certificate_attributes.default_or(),
..Default::default()
};
let spinner = ProgressBar::new_spinner().with_message("Creating certificate...");
spinner.enable_steady_tick(Duration::from_millis(100));
let certificate = client
.create_certificate(name, params.try_into()?, None)?
.await?
.into_model()?;
spinner.finish_and_clear();
let ResourceId { name, version, .. } = certificate.resource_id()?;
let certificate = client
.get_certificate(
&name,
Some(CertificateClientGetCertificateOptions {
certificate_version: version,
..Default::default()
}),
)
.await?
.into_model()?;
match output {
OutputFormat::Json => json::print(&certificate, global_args.color()),
OutputFormat::Default => show(&certificate),
}
}
#[tracing::instrument(level = Level::INFO, skip(self, global_args), fields(vault, name, version), err)]
async fn edit(&self, global_args: &crate::Args) -> Result<()> {
let Commands::Edit {
id,
vault,
name,
attributes:
AttributeArgs {
enabled,
expires,
not_before,
},
tags,
output,
} = self
else {
panic!("invalid command");
};
let (vault, name, version) = super::select(id.as_ref(), vault.as_ref(), name.as_ref())?;
let current = Span::current();
current.record("vault", &*vault);
current.record("name", &*name);
current.record("version", version.as_deref());
let client = CertificateClient::new(&vault, credential()?, None)?;
let certificate_attributes = CertificateAttributes {
enabled: Some(*enabled),
expires: *expires,
not_before: *not_before,
..Default::default()
};
let params = UpdateCertificatePropertiesParameters {
tags: map_tags(tags),
certificate_attributes: certificate_attributes.default_or(),
..Default::default()
};
let certificate = client
.update_certificate_properties(
&name,
params.try_into()?,
Some(CertificateClientUpdateCertificatePropertiesOptions {
certificate_version: version.map(Into::into),
..Default::default()
}),
)
.await?
.into_model()?;
match output {
OutputFormat::Json => json::print(&certificate, global_args.color()),
OutputFormat::Default => show(&certificate),
}
}
#[tracing::instrument(level = Level::INFO, skip(self), fields(vault, name, version), err)]
async fn edit_policy(&self, global_args: &crate::Args) -> Result<()> {
let Commands::EditPolicy {
id,
vault,
name,
issuer,
subject,
validity,
r#type,
size,
curve,
exportable,
reuse_key,
key_usage,
enhanced_key_usage,
} = self
else {
panic!("invalid command");
};
let (vault, name, version) = super::select(id.as_ref(), vault.as_ref(), name.as_ref())?;
let current = Span::current();
current.record("vault", &*vault);
current.record("name", &*name);
current.record("version", version.as_deref());
let client = CertificateClient::new(&vault, credential()?, None)?;
let key_properties = KeyProperties {
key_type: r#type.map(Into::into),
key_size: size.map(|value| *value),
curve: curve.map(Into::into),
exportable: exportable.map(Into::into),
reuse_key: reuse_key.map(Into::into),
};
let issuer_properties = IssuerParameters {
name: issuer.clone(),
..Default::default()
};
let x509_certificate_properties = X509CertificateProperties {
key_usage: map_vec(key_usage.as_deref(), Into::into),
enhanced_key_usage: map_vec(enhanced_key_usage.as_deref(), Clone::clone),
subject: subject.clone(),
validity_in_months: validity.map(|v| v as i32),
..Default::default()
};
let policy = CertificatePolicy {
key_properties: key_properties.default_or(),
issuer_parameters: issuer_properties.default_or(),
x509_certificate_properties: x509_certificate_properties.default_or(),
..Default::default()
};
let params = UpdateCertificatePropertiesParameters {
certificate_policy: policy.default_or(),
..Default::default()
};
let certificate = client
.update_certificate_properties(
&name,
params.try_into()?,
Some(CertificateClientUpdateCertificatePropertiesOptions {
certificate_version: version.map(Into::into),
..Default::default()
}),
)
.await?
.into_model()?;
if let Some(ref policy) = certificate.policy {
json::print(policy, global_args.color())?;
}
Ok(())
}
#[tracing::instrument(level = Level::INFO, skip(self, global_args), fields(vault, name, version), err)]
async fn get(&self, global_args: &crate::Args) -> Result<()> {
let Commands::Get {
id,
name,
vault,
output,
} = self
else {
panic!("invalid command");
};
let (vault, name, version) = super::select(id.as_ref(), vault.as_ref(), name.as_ref())?;
let current = Span::current();
current.record("vault", &*vault);
current.record("name", &*name);
current.record("version", version.as_deref());
let client = CertificateClient::new(&vault, credential()?, None)?;
let certificate = client
.get_certificate(
&name,
Some(CertificateClientGetCertificateOptions {
certificate_version: version.map(Into::into),
..Default::default()
}),
)
.await?
.into_model()?;
match output {
OutputFormat::Json => json::print(&certificate, global_args.color()),
OutputFormat::Default => show(&certificate),
}
}
#[tracing::instrument(level = Level::INFO, skip(self), fields(vault, name), err)]
async fn get_policy(&self, global_args: &crate::Args) -> std::result::Result<(), Error> {
let Commands::GetPolicy { id, name, vault } = self else {
panic!("Invalid command");
};
let (vault, name, ..) = super::select(id.as_ref(), vault.as_ref(), name.as_ref())?;
let current = Span::current();
current.record("vault", &*vault);
current.record("name", &*name);
let client = CertificateClient::new(&vault, credential()?, None)?;
let policy = client
.get_certificate_policy(&name, None)
.await?
.into_model()?;
json::print(&policy, global_args.color())
}
#[tracing::instrument(level = Level::INFO, skip(self), fields(vault), err)]
async fn list(&self, global_args: &crate::Args) -> Result<()> {
let Commands::List {
vault,
long,
output,
} = self
else {
panic!("invalid command");
};
Span::current().record("vault", vault.as_str());
let client = CertificateClient::new(vault.as_str(), credential()?, None)?;
let mut certificates: Vec<CertificateProperties> = client
.list_certificate_properties(None)?
.try_collect()
.await?;
certificates.sort_by(|a, b| a.id.cmp(&b.id));
if matches!(output, OutputFormat::Json) {
return json::print(&certificates, global_args.color());
}
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
let mut titles = Row::new(vec![
Cell::new("NAME").with_style(Attr::Dim),
Cell::new("ID").with_style(Attr::Dim),
]);
if *long {
titles.add_cell(Cell::new("CREATED").with_style(Attr::Dim));
}
titles.add_cell(Cell::new("EDITED").with_style(Attr::Dim));
table.set_titles(titles);
let now = OffsetDateTime::now_utc();
let formatter = Formatter::new();
let name_attr = Attr::ForegroundColor(color::GREEN);
for certificate in &certificates {
let resource: ResourceId = certificate.resource_id()?;
let source_id = resource.source_id;
let mut row = Row::new(vec![
Cell::new(resource.name.as_str()).with_style(name_attr),
Cell::new(source_id.as_str()),
]);
if *long {
let created = elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.created),
);
row.add_cell(Cell::new(created.as_str()));
}
let edited = elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.updated),
);
row.add_cell(Cell::new(edited.as_str()));
table.add_row(row);
}
table.print_color_conditionally(global_args.color())?;
Ok(())
}
#[tracing::instrument(level = Level::INFO, skip(self), fields(vault, name, version), err)]
async fn list_versions(&self, global_args: &crate::Args) -> Result<()> {
let Commands::ListVersions {
id,
name,
vault,
long,
output,
} = self
else {
panic!("invalid command");
};
let (vault, name, version) = super::select(id.as_ref(), vault.as_ref(), name.as_ref())?;
let current = Span::current();
current.record("vault", &*vault);
current.record("name", &*name);
current.record("version", version.as_deref());
let client = CertificateClient::new(&vault, credential()?, None)?;
let mut certificates: Vec<CertificateProperties> = client
.list_certificate_properties_versions(&name, None)?
.try_collect()
.await?;
certificates.sort_by(|a, b| {
let a = a.attributes.as_ref().and_then(|x| x.updated);
let b = b.attributes.as_ref().and_then(|x| x.updated);
a.cmp(&b).reverse()
});
if matches!(output, OutputFormat::Json) {
return json::print(&certificates, global_args.color());
}
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
let mut titles = Row::new(vec![Cell::new("ID").with_style(Attr::Dim)]);
if *long {
titles.add_cell(Cell::new("CREATED").with_style(Attr::Dim));
}
titles.add_cell(Cell::new("EDITED").with_style(Attr::Dim));
table.set_titles(titles);
let now = OffsetDateTime::now_utc();
let formatter = Formatter::new();
let id_attr = Attr::ForegroundColor(color::GREEN);
for certificate in &certificates {
let resource: ResourceId = certificate.resource_id()?;
let source_id = resource.source_id;
let mut row = Row::new(vec![Cell::new(source_id.as_str()).with_style(id_attr)]);
if *long {
let created = elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.created),
);
row.add_cell(Cell::new(created.as_str()));
}
let edited = elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.updated),
);
row.add_cell(Cell::new(edited.as_str()));
table.add_row(row);
}
table.print_color_conditionally(global_args.color())?;
Ok(())
}
}
fn show(certificate: &Certificate) -> Result<()> {
let resource = certificate.resource_id()?;
let now = OffsetDateTime::now_utc();
let formatter = Formatter::new();
println!("ID: {}", &resource.source_id);
println!("Name: {}", &resource.name);
println!("Version: {}", resource.version.unwrap_or_default());
println!(
"Thumbprint: {}",
certificate
.x509_thumbprint
.as_ref()
.map(|v| format!("{:X}", Bytes::copy_from_slice(v)))
.unwrap_or_default()
);
let x509_properties = certificate
.policy
.as_ref()
.and_then(|v| v.x509_certificate_properties.as_ref());
println!(
"Subject: {}",
x509_properties
.and_then(|v| v.subject.as_deref())
.unwrap_or_default()
);
let key_usage = x509_properties
.and_then(|v| v.key_usage.as_ref())
.map(|v| {
let mut c: Vec<String> = v
.iter()
.map(Into::<KeyUsageType>::into)
.map(|v| v.to_string())
.collect();
c.sort();
c
})
.unwrap_or_default();
println!("Key usage:");
for v in &key_usage {
println!(" {v}")
}
println!(
"Enabled: {}",
certificate
.attributes
.as_ref()
.and_then(|attr| attr.enabled)
.unwrap_or_default()
);
println!(
"Created: {}",
elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.created)
)
);
println!(
"Edited: {}",
elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.updated)
)
);
println!(
"Not before: {}",
elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.not_before)
)
);
println!(
"Expires: {}",
elapsed(
&formatter,
now,
certificate
.attributes
.as_ref()
.and_then(|attr| attr.expires)
)
);
println!("Tags:");
if let Some(tags) = &certificate.tags {
for (k, v) in tags {
println!(" {k}: {v}");
}
}
Ok(())
}
impl From<KeyType> for azure_security_keyvault_certificates::models::KeyType {
fn from(value: KeyType) -> Self {
match value {
KeyType::Ec => Self::Ec,
KeyType::EcHsm => Self::EcHsm,
KeyType::Rsa => Self::Rsa,
KeyType::RsaHsm => Self::RsaHsm,
}
}
}
impl From<&KeyType> for azure_security_keyvault_certificates::models::KeyType {
fn from(value: &KeyType) -> Self {
(*value).into()
}
}
impl From<CurveName> for azure_security_keyvault_certificates::models::CurveName {
fn from(value: CurveName) -> Self {
match value {
CurveName::P256 => Self::P256,
CurveName::P384 => Self::P384,
CurveName::P521 => Self::P521,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
pub enum KeyUsageType {
CRLSign,
DataEncipherment,
DecipherOnly,
DigitalSignature,
EncipherOnly,
KeyAgreement,
KeyCertSign,
KeyEncipherment,
NonRepudiation,
#[value(skip)]
UnknownValue(String),
}
impl fmt::Display for KeyUsageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.to_possible_value();
f.write_str(value.as_ref().map_or_else(|| "(unknown)", |v| v.get_name()))
}
}
impl From<&KeyUsageType> for azure_security_keyvault_certificates::models::KeyUsageType {
fn from(value: &KeyUsageType) -> Self {
match value {
KeyUsageType::CRLSign => Self::CRlSign,
KeyUsageType::DataEncipherment => Self::DataEncipherment,
KeyUsageType::DecipherOnly => Self::DecipherOnly,
KeyUsageType::DigitalSignature => Self::DigitalSignature,
KeyUsageType::EncipherOnly => Self::EncipherOnly,
KeyUsageType::KeyAgreement => Self::KeyAgreement,
KeyUsageType::KeyCertSign => Self::KeyCertSign,
KeyUsageType::KeyEncipherment => Self::KeyEncipherment,
KeyUsageType::NonRepudiation => Self::NonRepudiation,
KeyUsageType::UnknownValue(s) => Self::UnknownValue(s.clone()),
}
}
}
impl From<&azure_security_keyvault_certificates::models::KeyUsageType> for KeyUsageType {
fn from(value: &azure_security_keyvault_certificates::models::KeyUsageType) -> Self {
match value {
azure_security_keyvault_certificates::models::KeyUsageType::CRlSign => Self::CRLSign,
azure_security_keyvault_certificates::models::KeyUsageType::DataEncipherment => {
Self::DataEncipherment
}
azure_security_keyvault_certificates::models::KeyUsageType::DecipherOnly => {
Self::DecipherOnly
}
azure_security_keyvault_certificates::models::KeyUsageType::DigitalSignature => {
Self::DigitalSignature
}
azure_security_keyvault_certificates::models::KeyUsageType::EncipherOnly => {
Self::EncipherOnly
}
azure_security_keyvault_certificates::models::KeyUsageType::KeyAgreement => {
Self::KeyAgreement
}
azure_security_keyvault_certificates::models::KeyUsageType::KeyCertSign => {
Self::KeyCertSign
}
azure_security_keyvault_certificates::models::KeyUsageType::KeyEncipherment => {
Self::KeyEncipherment
}
azure_security_keyvault_certificates::models::KeyUsageType::NonRepudiation => {
Self::NonRepudiation
}
azure_security_keyvault_certificates::models::KeyUsageType::UnknownValue(s) => {
Self::UnknownValue(s.clone())
}
}
}
}
impl IsDefault for KeyProperties {
fn is_default(&self) -> bool {
self.curve.is_none()
&& self.exportable.is_none()
&& self.key_size.is_none()
&& self.key_type.is_none()
&& self.reuse_key.is_none()
}
}
impl IsDefault for IssuerParameters {
fn is_default(&self) -> bool {
self.certificate_transparency.is_none()
&& self.certificate_type.is_none()
&& self.name.is_none()
}
}
impl IsDefault for X509CertificateProperties {
fn is_default(&self) -> bool {
self.enhanced_key_usage.is_none()
&& self.key_usage.is_none()
&& self.subject.is_none()
&& self.subject_alternative_names.is_none()
&& self.validity_in_months.is_none()
}
}
impl IsDefault for CertificatePolicy {
fn is_default(&self) -> bool {
self.attributes.is_none()
&& self.id.is_none()
&& self.issuer_parameters.is_none()
&& self.key_properties.is_none()
&& self.lifetime_actions.is_none()
&& self.secret_properties.is_none()
&& self.x509_certificate_properties.is_none()
}
}
impl IsDefault for CertificateAttributes {
fn is_default(&self) -> bool {
self.enabled.is_none() && self.expires.is_none() && self.not_before.is_none()
}
}