use std::collections::BTreeMap;
use std::str::FromStr;
use std::time::Duration;
use async_trait::async_trait;
use clap::Args;
use colorful::Colorful;
use miette::{miette, IntoDiagnostic};
use tracing::debug;
use crate::shared_args::{IdentityOpts, RetryOpts, TrustOpts};
use crate::util::parsers::{duration_parser, duration_to_human_format};
use crate::{docs, Command, CommandGlobalOpts, Error, Result};
use ockam::Context;
use ockam_api::authenticator::direct::{
OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE, OCKAM_ROLE_ATTRIBUTE_KEY, OCKAM_TLS_ATTRIBUTE_KEY,
};
use ockam_api::authenticator::enrollment_tokens::{
TokenIssuer, DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_USAGE_COUNT, MAX_RECOMMENDED_TOKEN_DURATION,
MAX_RECOMMENDED_TOKEN_USAGE_COUNT,
};
use ockam_api::cli_state::{ExportedEnrollmentTicket, ProjectRoute};
use ockam_api::colors::color_primary;
use ockam_api::nodes::InMemoryNode;
use ockam_api::terminal::fmt;
use ockam_api::{fmt_info, fmt_log, fmt_ok, fmt_warn};
use ockam_multiaddr::MultiAddr;
const LONG_ABOUT: &str = include_str!("./static/ticket/long_about.txt");
const AFTER_LONG_HELP: &str = include_str!("./static/ticket/after_long_help.txt");
pub const OCKAM_RELAY_ATTRIBUTE: &str = "ockam-relay";
#[derive(Clone, Debug, Args)]
#[command(
long_about = docs::about(LONG_ABOUT),
after_long_help = docs::after_help(AFTER_LONG_HELP),
)]
pub struct TicketCommand {
#[command(flatten)]
identity_opts: IdentityOpts,
#[command(flatten)]
trust_opts: TrustOpts,
#[arg(short, long = "attribute", value_name = "ATTRIBUTE")]
attributes: Vec<String>,
#[arg(long = "expires-in", value_name = "DURATION", value_parser = duration_parser)]
expires_in: Option<Duration>,
#[arg(long = "usage-count", value_name = "USAGE_COUNT")]
usage_count: Option<u64>,
#[arg(long = "relay", value_name = "ENROLLEE_ALLOWED_RELAY_NAME")]
allowed_relay_name: Option<String>,
#[arg(long = "enroller")]
enroller: bool,
#[arg(long = "tls", hide = true)]
tls: bool,
#[command(flatten)]
retry_opts: RetryOpts,
#[arg(long = "hex", hide = true)]
hex_encoded: bool,
#[arg(long, hide = true)]
legacy: bool,
#[arg(long, hide = true)]
skip_controller_call: bool,
}
#[async_trait]
impl Command for TicketCommand {
const NAME: &'static str = "project ticket";
async fn run(self, ctx: &Context, opts: CommandGlobalOpts) -> Result<()> {
let cmd = self.parse_args(&opts).await?;
let identity = opts
.state
.get_identity_name_or_default(&cmd.identity_opts.identity_name)
.await?;
let node = InMemoryNode::start_with_project_name(
ctx,
&opts.state,
cmd.trust_opts.project_name.clone(),
)
.await?;
let project = opts
.state
.projects()
.get_project_by_name_or_default(&cmd.trust_opts.project_name)
.await?;
let authority_node_client = node
.create_authority_client_with_project(
ctx,
&project,
Some(identity),
cmd.skip_controller_call,
)
.await?;
let attributes = cmd.attributes()?;
debug!(attributes = ?attributes, "Attributes passed");
let token = {
let pb = opts.terminal.spinner();
if let Some(pb) = pb.as_ref() {
pb.set_message("Creating an enrollment ticket...");
}
authority_node_client
.create_token(ctx, attributes.clone(), cmd.expires_in, cmd.usage_count)
.await
.map_err(Error::Retry)?
};
let project = project.model();
let ticket = ExportedEnrollmentTicket::new(
token,
ProjectRoute::new(MultiAddr::from_str(&project.access_route)?)?,
project
.identity
.clone()
.ok_or(miette!("missing project's identity"))?,
&project.name,
project
.project_change_history
.as_ref()
.ok_or(miette!("missing project's change history"))?,
project
.authority_identity
.as_ref()
.ok_or(miette!("missing authority's change history"))?,
MultiAddr::from_str(
project
.authority_access_route
.as_ref()
.ok_or(miette!("missing authority's route"))?,
)?,
)
.import()
.await?;
let (as_json, encoded_ticket) = if cmd.legacy {
let exported = ticket.export_legacy()?;
(
serde_json::to_string(&exported).into_diagnostic()?,
exported.hex_encoded()?,
)
} else {
let exported = ticket.export()?;
let encoded = if cmd.hex_encoded {
exported.hex_encoded()?
} else {
exported.to_string()
};
(serde_json::to_string(&exported).into_diagnostic()?, encoded)
};
let usage_count = cmd.usage_count.unwrap_or(DEFAULT_TOKEN_USAGE_COUNT);
let attributes_msg = if attributes.is_empty() {
"".to_string()
} else {
let mut attributes_msg =
fmt_log!("The redeemer will be assigned the following attributes:\n");
let mut attributes: Vec<_> = attributes.iter().collect();
attributes.sort();
for (key, value) in &attributes {
attributes_msg += &fmt_log!(
"{}{}",
fmt::INDENTATION,
color_primary(format!("\"{key}={value}\"\n"))
);
}
attributes_msg += "\n";
attributes_msg
};
opts.terminal.write_line(
fmt_ok!("Created enrollment ticket\n\n")
+ &attributes_msg
+ &fmt_info!(
"It will expire in {} and it can be used {}\n",
color_primary(duration_to_human_format(
&cmd.expires_in.unwrap_or(DEFAULT_TOKEN_DURATION)
)),
if usage_count == 1 {
color_primary("once").to_string()
} else {
format!("up to {} times", color_primary(usage_count))
}
)
+ &fmt_log!(
"You can use it to enroll another machine using: {}",
color_primary("ockam project enroll")
),
)?;
opts.terminal
.to_stdout()
.plain(format!("\n{encoded_ticket}"))
.machine(encoded_ticket)
.json(as_json)
.write_line()?;
Ok(())
}
}
impl TicketCommand {
async fn parse_args(self, opts: &CommandGlobalOpts) -> miette::Result<Self> {
if let Some(usage_count) = self.usage_count {
if usage_count < 1 {
return Err(miette!("The usage count must be at least 1"));
}
}
if let (Some(expires_in), Some(usage_count)) = (self.expires_in, self.usage_count) {
if expires_in >= MAX_RECOMMENDED_TOKEN_DURATION
&& usage_count >= MAX_RECOMMENDED_TOKEN_USAGE_COUNT
{
opts.terminal.write_line(
fmt_warn!(
"You are creating a ticket with a long expiration time and a high usage count\n"
) + &fmt_log!(
"This is a security risk. Please consider reducing the values according to the ticket's intended use\n"
),
)?;
}
}
Ok(self)
}
fn attributes(&self) -> Result<BTreeMap<String, String>> {
let mut attributes = BTreeMap::new();
for attr in &self.attributes {
let mut parts = attr.splitn(2, '=');
let key = parts.next().ok_or(miette!("key expected"))?;
let value = parts.next().unwrap_or("true");
attributes.insert(key.to_string(), value.to_string());
}
if let Some(relay_name) = self.allowed_relay_name.clone() {
attributes.insert(OCKAM_RELAY_ATTRIBUTE.to_string(), relay_name);
}
if self.enroller {
attributes.insert(
OCKAM_ROLE_ATTRIBUTE_KEY.to_string(),
OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE.to_string(),
);
}
if self.tls {
attributes.insert(OCKAM_TLS_ATTRIBUTE_KEY.to_string(), "true".to_string());
}
Ok(attributes)
}
}