use crate::node::util::run_ockam;
use crate::util::{embedded_node_that_is_not_stopped, exitcode};
use crate::util::{local_cmd, node_rpc};
use crate::{docs, identity, CommandGlobalOpts, Result};
use clap::{ArgGroup, Args};
use miette::Context as _;
use miette::{miette, IntoDiagnostic};
use ockam::identity::{AttributesEntry, Identifier};
use ockam::Context;
use ockam_api::authority_node;
use ockam_api::authority_node::{OktaConfiguration, TrustedIdentity};
use ockam_api::bootstrapped_identities_store::PreTrustedIdentities;
use ockam_api::cli_state::init_node_state;
use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait};
use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType};
use ockam_api::DefaultAddress;
use ockam_core::compat::collections::HashMap;
use ockam_core::compat::fmt;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use tracing::debug;
const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt");
const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt");
const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt");
#[derive(Clone, Debug, Args)]
#[command(
long_about = docs::about(LONG_ABOUT),
before_help = docs::before_help(PREVIEW_TAG),
after_long_help = docs::after_help(AFTER_LONG_HELP),
)]
#[clap(group(ArgGroup::new("trusted").required(true).args(& ["trusted_identities", "reload_from_trusted_identities_file"])))]
pub struct CreateCommand {
#[arg(default_value = "authority")]
node_name: String,
#[arg(long, value_name = "PROJECT_IDENTIFIER")]
project_identifier: String,
#[arg(
display_order = 900,
long,
short,
id = "SOCKET_ADDRESS",
default_value = "127.0.0.1:4000"
)]
tcp_listener_address: String,
#[arg(long, hide = true)]
pub child_process: bool,
#[arg(long, value_name = "BOOL", default_value_t = false)]
no_direct_authentication: bool,
#[arg(long, default_value_t = false)]
no_token_enrollment: bool,
#[arg(group = "trusted", long, value_name = "JSON_OBJECT", value_parser = parse_trusted_identities)]
trusted_identities: Option<TrustedIdentities>,
#[arg(group = "trusted", long, value_name = "PATH")]
reload_from_trusted_identities_file: Option<PathBuf>,
#[arg(long, value_name = "URL", default_value = None)]
tenant_base_url: Option<String>,
#[arg(long, value_name = "STRING", default_value = None)]
certificate: Option<String>,
#[arg(long, value_name = "ATTRIBUTE_NAMES", default_value = None)]
attributes: Option<Vec<String>>,
#[arg(long, short, value_name = "BOOL", default_value_t = false)]
foreground: bool,
#[arg(long = "vault", value_name = "VAULT_NAME")]
vault: Option<String>,
#[arg(long = "identity", value_name = "IDENTITY_NAME")]
identity: Option<String>,
}
async fn spawn_background_node(
opts: &CommandGlobalOpts,
cmd: &CreateCommand,
) -> miette::Result<()> {
init_node_state(
&opts.state,
&cmd.node_name,
cmd.vault.as_deref(),
cmd.identity.as_deref(),
)
.await?;
let mut args = vec![
match opts.global_args.verbose {
0 => "-vv".to_string(),
v => format!("-{}", "v".repeat(v as usize)),
},
"authority".to_string(),
"create".to_string(),
"--project-identifier".to_string(),
cmd.project_identifier.clone(),
"--tcp-listener-address".to_string(),
cmd.tcp_listener_address.clone(),
"--foreground".to_string(),
"--child-process".to_string(),
];
if cmd.logging_to_file() || !opts.terminal.is_tty() {
args.push("--no-color".to_string());
}
if cmd.no_direct_authentication {
args.push("--no-direct-authentication".to_string());
}
if cmd.no_token_enrollment {
args.push("--no-token-enrollment".to_string());
}
if let Some(trusted_identities) = &cmd.trusted_identities {
args.push("--trusted-identities".to_string());
args.push(trusted_identities.to_string());
}
if let Some(reload_from_trusted_identities_file) = &cmd.reload_from_trusted_identities_file {
args.push("--reload-from-trusted-identities-file".to_string());
args.push(
reload_from_trusted_identities_file
.to_string_lossy()
.to_string(),
);
}
if let Some(tenant_base_url) = &cmd.tenant_base_url {
args.push("--tenant-base-url".to_string());
args.push(tenant_base_url.clone());
}
if let Some(certificate) = &cmd.certificate {
args.push("--certificate".to_string());
args.push(certificate.clone());
}
if let Some(attributes) = &cmd.attributes {
attributes.iter().for_each(|attr| {
args.push("--attributes".to_string());
args.push(attr.clone());
});
}
if let Some(vault) = &cmd.vault {
args.push("--vault".to_string());
args.push(vault.clone());
}
if let Some(identity) = &cmd.identity {
args.push("--identity".to_string());
args.push(identity.clone());
}
args.push(cmd.node_name.to_string());
run_ockam(opts, &cmd.node_name, args, cmd.logging_to_file())
}
impl CreateCommand {
pub fn run(self, options: CommandGlobalOpts) {
if self.foreground {
local_cmd(embedded_node_that_is_not_stopped(
start_authority_node,
(options, self),
))
} else {
node_rpc(create_background_node, (options, self))
}
}
pub(crate) fn trusted_identities(
&self,
authority_identifier: &Identifier,
) -> Result<PreTrustedIdentities> {
match (
&self.reload_from_trusted_identities_file,
&self.trusted_identities,
) {
(Some(path), None) => Ok(PreTrustedIdentities::ReloadFrom(path.clone())),
(None, Some(trusted)) => Ok(PreTrustedIdentities::Fixed(trusted.to_map(
self.project_identifier.to_string(),
authority_identifier,
))),
_ => Err(crate::Error::new(
exitcode::CONFIG,
miette!("Exactly one of 'reload-from-trusted-identities-file' or 'trusted-identities' must be defined"),
)),
}
}
pub fn logging_to_file(&self) -> bool {
if self.child_process {
true
}
else {
!self.foreground
}
}
}
async fn create_background_node(
_ctx: Context,
(opts, cmd): (CommandGlobalOpts, CreateCommand),
) -> miette::Result<()> {
spawn_background_node(&opts, &cmd).await
}
async fn start_authority_node(
ctx: Context,
args: (CommandGlobalOpts, CreateCommand),
) -> miette::Result<()> {
let (opts, cmd) = args;
if !opts.state.nodes.exists(&cmd.node_name) {
init_node_state(
&opts.state,
&cmd.node_name,
cmd.vault.as_deref(),
cmd.identity.as_deref(),
)
.await?;
};
let identifier = match &cmd.identity {
Some(identity_name) => {
debug!(name=%identity_name, "getting identity from state");
opts.state
.identities
.get(identity_name)
.context("Identity not found")?
.config()
.identifier()
}
None => {
debug!("getting default identity from state");
match opts.state.identities.default() {
Ok(state) => state.config().identifier(),
Err(_) => {
debug!("creating default identity");
let cmd = identity::CreateCommand::new("authority".into(), None, None);
cmd.create_identity(opts.clone()).await?
}
}
}
};
debug!(identifier=%identifier, "authority identifier");
let okta_configuration = match (&cmd.tenant_base_url, &cmd.certificate, &cmd.attributes) {
(Some(tenant_base_url), Some(certificate), Some(attributes)) => Some(OktaConfiguration {
address: DefaultAddress::OKTA_IDENTITY_PROVIDER.to_string(),
tenant_base_url: tenant_base_url.clone(),
certificate: certificate.clone(),
attributes: attributes.clone(),
}),
_ => None,
};
debug!("updating node state's setup config");
let node_state = opts.state.nodes.get(&cmd.node_name)?;
node_state.set_setup(
&node_state
.config()
.setup_mut()
.set_verbose(opts.global_args.verbose)
.set_authority_node()
.set_api_transport(
CreateTransportJson::new(
TransportType::Tcp,
TransportMode::Listen,
cmd.tcp_listener_address.as_str(),
)
.into_diagnostic()?,
),
)?;
let trusted_identities = cmd.trusted_identities(&identifier)?;
let configuration = authority_node::Configuration {
identifier,
storage_path: opts.state.identities.identities_repository_path()?,
vault_path: opts.state.vaults.default()?.vault_file_path().clone(),
project_identifier: cmd.project_identifier,
tcp_listener_address: cmd.tcp_listener_address,
secure_channel_listener_name: None,
authenticator_name: None,
trusted_identities,
no_direct_authentication: cmd.no_direct_authentication,
no_token_enrollment: cmd.no_token_enrollment,
okta: okta_configuration,
};
authority_node::start_node(&ctx, &configuration)
.await
.into_diagnostic()?;
Ok(())
}
fn parse_trusted_identities(values: &str) -> Result<TrustedIdentities> {
serde_json::from_str::<TrustedIdentities>(values).map_err(|e| {
crate::Error::new(
exitcode::CONFIG,
miette!("Cannot parse the trusted identities: {}", e),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use ockam::identity::Identifier;
use ockam_core::compat::collections::HashMap;
#[test]
fn test_parse_trusted_identities() {
let identity1 = Identifier::try_from("Ie86be15e83d1c93e24dd1967010b01b6df491b45").unwrap();
let identity2 = Identifier::try_from("I6c20e814b56579306f55c64e8747e6c1b4a53d9a").unwrap();
let trusted = format!("{{\"{identity1}\": {{\"name\": \"value\", \"trust_context_id\": \"1\"}}, \"{identity2}\": {{\"trust_context_id\" : \"1\", \"ockam-role\" : \"enroller\"}}}}");
let actual = parse_trusted_identities(trusted.as_str()).unwrap();
let attributes1 = HashMap::from([
("name".into(), "value".into()),
("trust_context_id".into(), "1".into()),
]);
let attributes2 = HashMap::from([
("trust_context_id".into(), "1".into()),
("ockam-role".into(), "enroller".into()),
]);
let expected = vec![
TrustedIdentity::new(&identity2, &attributes2),
TrustedIdentity::new(&identity1, &attributes1),
];
assert_eq!(actual.trusted_identities(), expected);
}
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
struct TrustedIdentities(HashMap<Identifier, HashMap<String, String>>);
impl TrustedIdentities {
pub fn trusted_identities(&self) -> Vec<TrustedIdentity> {
self.0
.iter()
.map(|(k, v)| TrustedIdentity::new(k, v))
.collect()
}
pub(crate) fn to_map(
&self,
project_identifier: String,
authority_identifier: &Identifier,
) -> HashMap<Identifier, AttributesEntry> {
HashMap::from_iter(self.trusted_identities().iter().map(|t| {
(
t.identifier(),
t.attributes_entry(project_identifier.clone(), authority_identifier),
)
}))
}
}
impl Display for TrustedIdentities {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(
serde_json::to_string(self)
.map_err(|_| fmt::Error)?
.as_str(),
)
}
}