canic-host 0.70.3

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use crate::deployment_truth::{
    DEPLOYMENT_TRUTH_SCHEMA_VERSION, PromotionMaterializationIdentityReportV1,
    PromotionMaterializationOutputGroupV1, PromotionReadinessStatusV1,
    RolePromotionMaterializationIdentityV1, SafetyFindingV1, SafetySeverityV1,
};
use std::collections::BTreeSet;

use super::super::digest::promotion_materialization_identity_report_digest;
use super::super::ensure::{
    ensure_materialization_report_field, ensure_materialization_report_sha256,
};
use super::super::error::PromotionMaterializationIdentityReportError;
use super::super::identity::{
    materialization_output_key_for_group, materialization_output_key_for_role,
};

pub fn validate_promotion_materialization_identity_report(
    report: &PromotionMaterializationIdentityReportV1,
) -> Result<(), PromotionMaterializationIdentityReportError> {
    if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
        return Err(
            PromotionMaterializationIdentityReportError::SchemaVersionMismatch {
                expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
                found: report.schema_version,
            },
        );
    }
    ensure_materialization_report_field("report_id", &report.report_id)?;
    ensure_materialization_report_sha256(
        "materialization_identity_report_digest",
        &report.materialization_identity_report_digest,
    )?;
    ensure_materialization_report_status_matches_blockers(report)?;
    ensure_unique_materialization_report_roles(&report.roles)?;
    for role in &report.roles {
        validate_role_materialization_identity(role)?;
    }
    validate_materialization_output_groups(&report.roles, &report.output_groups)?;
    let expected_blockers = Vec::<SafetyFindingV1>::new();
    if report.blockers != expected_blockers {
        return Err(PromotionMaterializationIdentityReportError::BlockerMismatch);
    }
    validate_materialization_report_blockers(&report.blockers)?;
    if report.materialization_identity_report_digest
        != promotion_materialization_identity_report_digest(report)
    {
        return Err(
            PromotionMaterializationIdentityReportError::LinkageMismatch {
                field: "materialization_identity_report_digest",
            },
        );
    }
    Ok(())
}

fn validate_materialization_output_groups(
    roles: &[RolePromotionMaterializationIdentityV1],
    groups: &[PromotionMaterializationOutputGroupV1],
) -> Result<(), PromotionMaterializationIdentityReportError> {
    let role_names = roles
        .iter()
        .map(|role| role.role.as_str())
        .collect::<BTreeSet<_>>();
    let mut grouped_roles = BTreeSet::new();
    let mut group_keys = BTreeSet::new();
    for group in groups {
        validate_materialization_output_group(group)?;
        if !group_keys.insert(group.output_identity_key.as_str()) {
            return Err(
                PromotionMaterializationIdentityReportError::DuplicateOutputGroup {
                    output_identity_key: group.output_identity_key.clone(),
                },
            );
        }
        if group.roles.is_empty() {
            return Err(
                PromotionMaterializationIdentityReportError::EmptyOutputGroup {
                    output_identity_key: group.output_identity_key.clone(),
                },
            );
        }
        for role in &group.roles {
            if !role_names.contains(role.as_str()) {
                return Err(
                    PromotionMaterializationIdentityReportError::UnknownGroupedRole {
                        role: role.clone(),
                    },
                );
            }
            if !grouped_roles.insert(role.as_str()) {
                return Err(
                    PromotionMaterializationIdentityReportError::DuplicateGroupedRole {
                        role: role.clone(),
                    },
                );
            }
            let role_identity = roles
                .iter()
                .find(|candidate| candidate.role == *role)
                .expect("known role should be present");
            let expected = materialization_output_key_for_role(role_identity);
            if expected != group.output_identity_key {
                return Err(
                    PromotionMaterializationIdentityReportError::OutputGroupRoleMismatch {
                        role: role.clone(),
                        expected,
                        found: group.output_identity_key.clone(),
                    },
                );
            }
        }
    }
    for role in roles {
        if !grouped_roles.contains(role.role.as_str()) {
            return Err(
                PromotionMaterializationIdentityReportError::MissingGroupedRole {
                    role: role.role.clone(),
                },
            );
        }
    }
    Ok(())
}

fn validate_materialization_output_group(
    group: &PromotionMaterializationOutputGroupV1,
) -> Result<(), PromotionMaterializationIdentityReportError> {
    ensure_materialization_report_field(
        "output_group.output_identity_key",
        &group.output_identity_key,
    )?;
    ensure_materialization_report_sha256("output_group.wasm_sha256", &group.wasm_sha256)?;
    ensure_materialization_report_sha256("output_group.wasm_gz_sha256", &group.wasm_gz_sha256)?;
    ensure_materialization_report_sha256(
        "output_group.installed_module_hash",
        &group.installed_module_hash,
    )?;
    ensure_materialization_report_sha256("output_group.candid_sha256", &group.candid_sha256)?;
    let expected = materialization_output_key_for_group(group);
    if expected != group.output_identity_key {
        return Err(
            PromotionMaterializationIdentityReportError::OutputGroupKeyMismatch {
                expected,
                found: group.output_identity_key.clone(),
            },
        );
    }
    Ok(())
}

const fn ensure_materialization_report_status_matches_blockers(
    report: &PromotionMaterializationIdentityReportV1,
) -> Result<(), PromotionMaterializationIdentityReportError> {
    match (report.status, report.blockers.is_empty()) {
        (PromotionReadinessStatusV1::Ready, false)
        | (PromotionReadinessStatusV1::Blocked, true) => Err(
            PromotionMaterializationIdentityReportError::StatusBlockerMismatch {
                status: report.status,
                blocker_count: report.blockers.len(),
            },
        ),
        _ => Ok(()),
    }
}

fn ensure_unique_materialization_report_roles(
    roles: &[RolePromotionMaterializationIdentityV1],
) -> Result<(), PromotionMaterializationIdentityReportError> {
    let mut seen_roles = BTreeSet::new();
    let mut seen_evidence = BTreeSet::new();
    for role in roles {
        if !seen_roles.insert(role.role.as_str()) {
            return Err(PromotionMaterializationIdentityReportError::DuplicateRole {
                role: role.role.clone(),
            });
        }
        if !seen_evidence.insert(role.evidence_id.as_str()) {
            return Err(
                PromotionMaterializationIdentityReportError::DuplicateEvidence {
                    evidence_id: role.evidence_id.clone(),
                },
            );
        }
    }
    Ok(())
}

fn validate_role_materialization_identity(
    role: &RolePromotionMaterializationIdentityV1,
) -> Result<(), PromotionMaterializationIdentityReportError> {
    ensure_materialization_report_field("role", &role.role)?;
    ensure_materialization_report_field("evidence_id", &role.evidence_id)?;
    ensure_materialization_report_sha256(
        "materialization_evidence_digest",
        &role.materialization_evidence_digest,
    )?;
    ensure_materialization_report_field("recipe_id", &role.recipe_id)?;
    ensure_materialization_report_field(
        "materialization_input_id",
        &role.materialization_input_id,
    )?;
    ensure_materialization_report_field(
        "materialization_result_id",
        &role.materialization_result_id,
    )?;
    ensure_materialization_report_sha256(
        "materialization_input_digest",
        &role.materialization_input_digest,
    )?;
    ensure_materialization_report_sha256(
        "canonical_embedded_config_sha256",
        &role.canonical_embedded_config_sha256,
    )?;
    ensure_materialization_report_field("network", &role.network)?;
    ensure_materialization_report_field("root_trust_anchor", &role.root_trust_anchor)?;
    ensure_materialization_report_field("runtime_variant", &role.runtime_variant)?;
    ensure_materialization_report_sha256("wasm_sha256", &role.wasm_sha256)?;
    ensure_materialization_report_sha256("wasm_gz_sha256", &role.wasm_gz_sha256)?;
    ensure_materialization_report_sha256("installed_module_hash", &role.installed_module_hash)?;
    ensure_materialization_report_sha256("candid_sha256", &role.candid_sha256)?;
    Ok(())
}

fn validate_materialization_report_blockers(
    blockers: &[SafetyFindingV1],
) -> Result<(), PromotionMaterializationIdentityReportError> {
    for blocker in blockers {
        ensure_materialization_report_field("blocker.code", &blocker.code)?;
        ensure_materialization_report_field("blocker.message", &blocker.message)?;
        if blocker.severity != SafetySeverityV1::HardFailure {
            return Err(
                PromotionMaterializationIdentityReportError::BlockerSeverityMismatch {
                    severity: blocker.severity,
                },
            );
        }
    }
    Ok(())
}