silicon-iam-cli 1.1.0

Command-line client for Silicon IAM, built on the silicon-iam-client crate.
//! Advisory trust.

use silicon_iam_client::models;

use crate::{
    cli::TrustCommand,
    context::Context,
    error::{CliError, Result},
    output::{Format, Table, json, plain},
};

/// Parses a boundary and level into the contract's trust value.
///
/// Shared with invitations, which set a new member's starting trust.
///
/// # Errors
///
/// Returns [`CliError::Usage`] for a value outside the closed vocabulary.
///
/// Runs a trust command.
///
/// # Errors
///
/// Returns whatever the service reports.
#[allow(
    clippy::too_many_lines,
    reason = "one arm per verb; splitting the match would only move it elsewhere"
)]
pub async fn run(context: &Context, command: TrustCommand) -> Result<()> {
    let client = context.authenticated().await?;
    let org = context.organization()?;
    match command {
        TrustCommand::Default => {
            let value = client.trust().default(org).await?;
            report_value(context, &value)
        }
        TrustCommand::SetDefault { boundary, level } => {
            // The default resource carries no version of its own, so the
            // organization's is what guards the replacement.
            let organization = client.organizations().get(org).await?;
            let replaced = client
                .trust()
                .replace_default(
                    org,
                    organization.version,
                    &trust_value(&boundary, &level)?,
                    &context.mutation(),
                )
                .await?;
            report_value(context, &replaced)
        }
        TrustCommand::List(page) => {
            let listed = client.trust().list_rules(org, &page.paging()).await?;
            match context.format {
                Format::Json => json(&listed),
                Format::Text => {
                    let mut table = Table::new(["id", "subject", "target", "trust", "version"]);
                    for rule in &listed.items {
                        table.row([
                            rule.id.to_string(),
                            selector(&rule.subject),
                            selector(&rule.target),
                            format!("{:?}/{:?}", rule.trust.boundary, rule.trust.level)
                                .to_lowercase(),
                            rule.version.to_string(),
                        ]);
                    }
                    table.print();
                    Ok(())
                }
            }
        }
        TrustCommand::Create {
            subject_tag,
            subject_membership,
            target_tag,
            target_membership,
            boundary,
            level,
        } => {
            let created = client
                .trust()
                .create_rule(
                    org,
                    &models::TrustRuleCreate {
                        subject: build_selector(subject_tag, subject_membership, "subject")?,
                        target: build_selector(target_tag, target_membership, "target")?,
                        trust: trust_value(&boundary, &level)?,
                    },
                    &context.mutation(),
                )
                .await?;
            report_rule(context, &created)
        }
        TrustCommand::Show { rule_id } => {
            let rule = client.trust().get_rule(org, rule_id).await?;
            report_rule(context, &rule)
        }
        TrustCommand::Update {
            rule_id,
            boundary,
            level,
        } => {
            let current = client.trust().get_rule(org, rule_id).await?;
            let updated = client
                .trust()
                .update_rule(
                    org,
                    rule_id,
                    current.version,
                    &models::TrustRulePatch {
                        subject: None,
                        target: None,
                        trust: Some(trust_value(&boundary, &level)?),
                    },
                    &context.mutation(),
                )
                .await?;
            report_rule(context, &updated)
        }
        TrustCommand::Delete { rule_id } => {
            let current = client.trust().get_rule(org, rule_id).await?;
            client
                .trust()
                .delete_rule(org, rule_id, current.version, &context.mutation())
                .await?;
            println!("Archived {rule_id}.");
            Ok(())
        }
        TrustCommand::Evaluate { subject, target } => {
            let evaluation = client
                .trust()
                .evaluate(
                    org,
                    &models::TrustEvaluationRequest {
                        subject_membership_id: subject,
                        target_silicon_membership_id: target,
                    },
                )
                .await?;
            match context.format {
                Format::Json => json(&evaluation),
                Format::Text => {
                    let mut table = Table::new(["field", "value"]);
                    table.row([
                        "trust",
                        &format!(
                            "{:?}/{:?}",
                            evaluation.trust.boundary, evaluation.trust.level
                        )
                        .to_lowercase(),
                    ]);
                    table.row(["advisory", &plain(&evaluation.advisory)]);
                    table.row(["source", &format!("{:?}", evaluation.source).to_lowercase()]);
                    table.row([
                        "matched_rules",
                        &evaluation
                            .matching_rule_ids
                            .iter()
                            .map(ToString::to_string)
                            .collect::<Vec<_>>()
                            .join(", "),
                    ]);
                    table.print();
                    Ok(())
                }
            }
        }
    }
}

fn build_selector(
    tag: Option<uuid::Uuid>,
    membership: Option<uuid::Uuid>,
    side: &str,
) -> Result<models::TrustSelector> {
    match (tag, membership) {
        (Some(tag), None) => Ok(models::TrustSelector::tag(tag)),
        (None, Some(membership)) => Ok(models::TrustSelector::membership(membership)),
        _ => Err(CliError::Usage(format!(
            "give exactly one of --{side}-tag or --{side}-membership"
        ))),
    }
}

pub fn trust_value(boundary: &str, level: &str) -> Result<models::TrustValue> {
    let boundary = match boundary {
        "internal" => models::TrustValueBoundary::Internal,
        "external" => models::TrustValueBoundary::External,
        other => {
            return Err(CliError::Usage(format!(
                "unknown boundary `{other}`; expected internal or external"
            )));
        }
    };
    let level = match level {
        "not_trusted" => models::TrustValueLevel::NotTrusted,
        "needs_approval" => models::TrustValueLevel::NeedsApproval,
        "trusted" => models::TrustValueLevel::Trusted,
        other => {
            return Err(CliError::Usage(format!(
                "unknown level `{other}`; expected not_trusted, needs_approval or trusted"
            )));
        }
    };
    Ok(models::TrustValue { boundary, level })
}

fn selector(selector: &models::TrustSelector) -> String {
    match selector {
        models::TrustSelector::Tag { tag_id } => format!("tag:{tag_id}"),
        models::TrustSelector::Membership { membership_id } => {
            format!("membership:{membership_id}")
        }
    }
}

fn report_value(context: &Context, value: &models::TrustValue) -> Result<()> {
    match context.format {
        Format::Json => json(value),
        Format::Text => {
            println!(
                "{}",
                format!("{:?}/{:?}", value.boundary, value.level).to_lowercase()
            );
            Ok(())
        }
    }
}

fn report_rule(context: &Context, rule: &models::TrustRule) -> Result<()> {
    match context.format {
        Format::Json => json(rule),
        Format::Text => {
            let mut table = Table::new(["field", "value"]);
            table.row(["id", &rule.id.to_string()]);
            table.row(["subject", &selector(&rule.subject)]);
            table.row(["target", &selector(&rule.target)]);
            table.row([
                "trust",
                &format!("{:?}/{:?}", rule.trust.boundary, rule.trust.level).to_lowercase(),
            ]);
            table.row(["version", &rule.version.to_string()]);
            table.print();
            Ok(())
        }
    }
}