use std::collections::BTreeMap;
use std::time::Duration;
use clap::Args;
use colorful::Colorful;
use miette::{miette, IntoDiagnostic};
use ockam::identity::Identifier;
use ockam::Context;
use ockam_api::authenticator::direct::{
Members, OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE, OCKAM_ROLE_ATTRIBUTE_KEY,
};
use ockam_api::authenticator::enrollment_tokens::TokenIssuer;
use ockam_api::cli_state::enrollments::EnrollmentTicket;
use ockam_api::cli_state::CliState;
use ockam_api::cloud::project::Project;
use ockam_api::nodes::InMemoryNode;
use ockam_multiaddr::{proto, MultiAddr, Protocol};
use crate::fmt_ok;
use crate::util::async_cmd;
use crate::{docs, CommandGlobalOpts, Result};
use crate::{
output::OutputFormat,
util::api::{CloudOpts, TrustOpts},
};
use crate::{terminal::color_primary, util::duration::duration_parser};
use ockam_api::cloud::project::models::ProjectModel;
use tracing::debug;
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)]
cloud_opts: CloudOpts,
#[command(flatten)]
trust_opts: TrustOpts,
#[arg(value_name = "IDENTIFIER", long, short, conflicts_with = "expires_in")]
member: Option<Identifier>,
#[arg(
long,
short,
default_value = "/project/default",
value_name = "ROUTE_TO_PROJECT"
)]
to: MultiAddr,
#[arg(short, long = "attribute", value_name = "ATTRIBUTE")]
attributes: Vec<String>,
#[arg(long = "expires-in", value_name = "DURATION", conflicts_with = "member", value_parser = duration_parser)]
expires_in: Option<Duration>,
#[arg(
long = "usage-count",
value_name = "USAGE_COUNT",
conflicts_with = "member"
)]
usage_count: Option<u64>,
#[arg(long = "relay", value_name = "ENROLLEE_ALLOWED_RELAY_NAME")]
allowed_relay_name: Option<String>,
#[arg(long = "enroller")]
enroller: bool,
}
impl TicketCommand {
pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> {
async_cmd(&self.name(), opts.clone(), |ctx| async move {
self.async_run(&ctx, opts).await
})
}
pub fn name(&self) -> String {
"create project ticket".into()
}
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().ok_or(miette!("value expected)"))?;
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(),
);
}
Ok(attributes)
}
async fn async_run(&self, ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> {
if opts.global_args.output_format == OutputFormat::Json {
return Err(miette::miette!(
"This command only outputs a hex encoded string for 'ockam project enroll' to use. \
Please try running it again without '--output json'."
));
}
let node = InMemoryNode::start_with_project_name(
ctx,
&opts.state,
self.trust_opts.project_name.clone(),
)
.await?;
let project_model: Option<ProjectModel>;
let authority_node_client = if let Some(p) = get_project(&opts.state, &self.to).await? {
let identity = opts
.state
.get_identity_name_or_default(&self.cloud_opts.identity)
.await?;
project_model = Some(p.model().clone());
node.create_authority_client(
&p.authority_identifier().into_diagnostic()?,
p.authority_multiaddr().into_diagnostic()?,
Some(identity),
)
.await?
} else {
return Err(miette!("Cannot create a ticket. Please specify a route to your project or to an authority node"));
};
let attributes = self.attributes()?;
debug!(attributes = ?attributes, "Attributes passed");
if let Some(id) = &self.member {
authority_node_client
.add_member(ctx, id.clone(), attributes)
.await?
} else {
let token = authority_node_client
.create_token(ctx, attributes, self.expires_in, self.usage_count)
.await?;
let ticket = EnrollmentTicket::new(token, project_model);
let ticket_serialized = ticket.hex_encoded().into_diagnostic()?;
opts.terminal.write_line(&fmt_ok!(
"{}: {}",
"Created enrollment ticket. You can use it to enroll another machine using",
color_primary("ockam project enroll")
))?;
opts.terminal
.clone()
.stdout()
.machine(ticket_serialized)
.write_line()?;
}
Ok(())
}
}
async fn get_project(cli_state: &CliState, input: &MultiAddr) -> Result<Option<Project>> {
if let Some(proto) = input.first() {
if proto.code() == proto::Project::CODE {
let project_name = proto.cast::<proto::Project>().expect("project protocol");
match cli_state.projects().get_project_by_name(&project_name).await.ok() {
None => Err(miette!("Unknown project '{}'. Run 'ockam project list' to get a list of available projects.", project_name.to_string()))?,
Some(project) => {
Ok(Some(project))
}
}
} else {
Ok(None)
}
} else {
Ok(None)
}
}