trazaeo 0.5.2

Open-source provenance SDK and specification for verifiable EO and climate data workflows
Documentation
use crate::error::{TrazaeoError, TrazaeoResult};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustPolicy {
    allowed_keys: HashSet<String>,
    revoked_keys: HashSet<String>,
    audit_log: Vec<TrustDecision>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustDecision {
    pub action: String,
    pub key_id: String,
    pub reason: String,
    pub effective_at: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustStatus {
    Trusted,
    Revoked,
    Untrusted,
}

impl TrustPolicy {
    /// Creates a new instance.
    pub fn new() -> Self {
        Self {
            allowed_keys: HashSet::new(),
            revoked_keys: HashSet::new(),
            audit_log: Vec::new(),
        }
    }

    /// Allows key.
    pub fn allow_key(&mut self, key_id: &str) {
        self.allow_key_with_reason(key_id, "allowlist", "unspecified");
    }

    /// Revokes key.
    pub fn revoke_key(&mut self, key_id: &str) {
        self.revoke_key_with_reason(key_id, "revoke", "unspecified");
    }

    /// Allows key with reason.
    pub fn allow_key_with_reason(&mut self, key_id: &str, effective_at: &str, reason: &str) {
        self.allowed_keys.insert(key_id.to_string());
        self.audit_log.push(TrustDecision {
            action: "allow".to_string(),
            key_id: key_id.to_string(),
            reason: reason.to_string(),
            effective_at: effective_at.to_string(),
        });
    }

    /// Revokes key with reason.
    pub fn revoke_key_with_reason(&mut self, key_id: &str, effective_at: &str, reason: &str) {
        self.revoked_keys.insert(key_id.to_string());
        self.audit_log.push(TrustDecision {
            action: "revoke".to_string(),
            key_id: key_id.to_string(),
            reason: reason.to_string(),
            effective_at: effective_at.to_string(),
        });
    }

    /// Rotates key.
    pub fn rotate_key(&mut self, old_key: &str, new_key: &str) {
        self.revoke_key_with_reason(old_key, "rotate", "rotated to successor key");
        self.allow_key_with_reason(new_key, "rotate", "rotated from predecessor key");
    }

    /// Returns trust information for status.
    pub fn trust_status(&self, key_id: &str) -> TrustStatus {
        if self.revoked_keys.contains(key_id) {
            TrustStatus::Revoked
        } else if self.allowed_keys.contains(key_id) {
            TrustStatus::Trusted
        } else {
            TrustStatus::Untrusted
        }
    }

    /// Handles audit log.
    pub fn audit_log(&self) -> &[TrustDecision] {
        &self.audit_log
    }

    /// Saves to path.
    pub fn save_to_path<P: AsRef<Path>>(&self, path: P) -> TrazaeoResult<()> {
        let body = serde_json::to_vec_pretty(self).map_err(|e| {
            TrazaeoError::serialization("save trust policy", format!("serialize trust policy: {e}"))
        })?;
        std::fs::write(path, body)
            .map_err(|e| TrazaeoError::io("save trust policy", format!("write trust policy: {e}")))
    }

    /// Loads from path.
    pub fn load_from_path<P: AsRef<Path>>(path: P) -> TrazaeoResult<Self> {
        let body = std::fs::read(path).map_err(|e| {
            TrazaeoError::io("load trust policy", format!("read trust policy: {e}"))
        })?;
        serde_json::from_slice(&body).map_err(|e| {
            TrazaeoError::serialization(
                "load trust policy",
                format!("deserialize trust policy: {e}"),
            )
        })
    }
}

impl Default for TrustPolicy {
    /// Builds the default value for this type.
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::NamedTempFile;

    /// Tests that trust policy allows and revokes keys.
    #[test]
    fn trust_policy_allows_and_revokes_keys() {
        let mut policy = TrustPolicy::new();
        policy.allow_key("k1");
        assert_eq!(policy.trust_status("k1"), TrustStatus::Trusted);

        policy.revoke_key("k1");
        assert_eq!(policy.trust_status("k1"), TrustStatus::Revoked);
    }

    /// Tests that trust policy rotation marks old revoked and new trusted.
    #[test]
    fn trust_policy_rotation_marks_old_revoked_and_new_trusted() {
        let mut policy = TrustPolicy::new();
        policy.allow_key("old");
        policy.rotate_key("old", "new");

        assert_eq!(policy.trust_status("old"), TrustStatus::Revoked);
        assert_eq!(policy.trust_status("new"), TrustStatus::Trusted);
    }

    /// Tests that unknown key is untrusted.
    #[test]
    fn unknown_key_is_untrusted() {
        let policy = TrustPolicy::new();
        assert_eq!(policy.trust_status("missing"), TrustStatus::Untrusted);
    }

    /// Tests that trust policy persists to disk.
    #[test]
    fn trust_policy_persists_to_disk() {
        let mut policy = TrustPolicy::new();
        policy.allow_key("k1");
        policy.revoke_key("k2");

        let file = NamedTempFile::new().expect("temp file");
        policy.save_to_path(file.path()).expect("save trust policy");

        let loaded = TrustPolicy::load_from_path(file.path()).expect("load trust policy");
        assert_eq!(loaded.trust_status("k1"), TrustStatus::Trusted);
        assert_eq!(loaded.trust_status("k2"), TrustStatus::Revoked);
    }

    /// Tests that trust policy records audit log entries.
    #[test]
    fn trust_policy_records_audit_log_entries() {
        let mut policy = TrustPolicy::new();
        policy.allow_key_with_reason("k1", "2026-01-01T00:00:00Z", "bootstrap allow");
        policy.revoke_key_with_reason("k1", "2026-01-02T00:00:00Z", "compromised");

        assert_eq!(policy.audit_log().len(), 2);
        assert_eq!(policy.audit_log()[0].action, "allow");
        assert_eq!(policy.audit_log()[1].action, "revoke");
    }
}