tsafe-core 1.0.11

Encrypted local secret vault library — AES-256 via age, audit log, RBAC, biometric keyring, CloudEvents
Documentation
//! Explicit deny reason codes for auditable exec policy enforcement.
//!
//! Every time an exec operation is denied (contract target check, missing required
//! secret, dangerous env variable, etc.), the deny is classified with a specific
//! reason code and message. This makes the audit log deterministic and helps operators
//! understand why a command was blocked.

use serde::{Deserialize, Serialize};
use std::fmt;

/// Specific reason why an exec operation was denied.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DenyReason {
    /// Contract target allowlist rejected the command (not in list, no match by
    /// exact name or basename).
    TargetNotAllowed,

    /// Command was empty or not provided, but the contract requires a target.
    TargetMissing,

    /// A secret in the contract's required_secrets list does not exist in the vault.
    RequiredSecretNotFound,

    /// An injected env var name is in the dangerous list (NODE_OPTIONS, LD_PRELOAD, etc.)
    /// and exec --deny-dangerous-env was active.
    DangerousEnvVariable,

    /// The vault profile specified in the contract does not exist or cannot be opened.
    VaultProfileNotFound,

    /// The namespace specified in the contract does not exist in the vault.
    NamespaceMissing,

    /// The access profile (read_only / read_write) cannot be enforced on this operation.
    AccessProfileViolation,

    /// A secret specified in allowed_secrets is not present in the vault.
    AllowedSecretNotFound,

    /// Network policy forbids this operation (future use).
    NetworkPolicyViolation,

    /// Implicit deny: no contract was provided and --keys/--ns were too restrictive.
    InsufficientAuthority,
}

impl DenyReason {
    /// Return a human-readable short message explaining the deny.
    pub fn message(&self) -> &'static str {
        match self {
            Self::TargetNotAllowed => "command not in contract target allowlist",
            Self::TargetMissing => "command required by contract but not provided",
            Self::RequiredSecretNotFound => "required vault secret does not exist",
            Self::DangerousEnvVariable => {
                "environment variable is high-risk; use --allow-dangerous-env to override"
            }
            Self::VaultProfileNotFound => "vault profile not found or cannot be opened",
            Self::NamespaceMissing => "namespace does not exist in vault",
            Self::AccessProfileViolation => {
                "operation violates access profile (read-only) constraint"
            }
            Self::AllowedSecretNotFound => "secret in contract allowlist does not exist in vault",
            Self::NetworkPolicyViolation => "operation violates network policy",
            Self::InsufficientAuthority => {
                "insufficient authority: add --keys or --ns or use a contract"
            }
        }
    }

    /// Return the reason code as a constant string.
    pub fn code(&self) -> &'static str {
        match self {
            Self::TargetNotAllowed => "TARGET_NOT_ALLOWED",
            Self::TargetMissing => "TARGET_MISSING",
            Self::RequiredSecretNotFound => "REQUIRED_SECRET_NOT_FOUND",
            Self::DangerousEnvVariable => "DANGEROUS_ENV_VARIABLE",
            Self::VaultProfileNotFound => "VAULT_PROFILE_NOT_FOUND",
            Self::NamespaceMissing => "NAMESPACE_MISSING",
            Self::AccessProfileViolation => "ACCESS_PROFILE_VIOLATION",
            Self::AllowedSecretNotFound => "ALLOWED_SECRET_NOT_FOUND",
            Self::NetworkPolicyViolation => "NETWORK_POLICY_VIOLATION",
            Self::InsufficientAuthority => "INSUFFICIENT_AUTHORITY",
        }
    }
}

impl fmt::Display for DenyReason {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.code(), self.message())
    }
}

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

    #[test]
    fn deny_reasons_have_unique_codes() {
        let codes = [
            DenyReason::TargetNotAllowed.code(),
            DenyReason::TargetMissing.code(),
            DenyReason::RequiredSecretNotFound.code(),
            DenyReason::DangerousEnvVariable.code(),
            DenyReason::VaultProfileNotFound.code(),
            DenyReason::NamespaceMissing.code(),
            DenyReason::AccessProfileViolation.code(),
            DenyReason::AllowedSecretNotFound.code(),
            DenyReason::NetworkPolicyViolation.code(),
            DenyReason::InsufficientAuthority.code(),
        ];
        assert_eq!(
            codes.len(),
            codes.iter().collect::<std::collections::HashSet<_>>().len()
        );
    }

    #[test]
    fn deny_reason_display_is_deterministic() {
        let reason = DenyReason::TargetNotAllowed;
        let display = format!("{reason}");
        assert_eq!(
            display,
            "TARGET_NOT_ALLOWED: command not in contract target allowlist"
        );
    }
}