use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DenyReason {
TargetNotAllowed,
TargetMissing,
RequiredSecretNotFound,
DangerousEnvVariable,
VaultProfileNotFound,
NamespaceMissing,
AccessProfileViolation,
AllowedSecretNotFound,
NetworkPolicyViolation,
InsufficientAuthority,
}
impl DenyReason {
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"
}
}
}
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"
);
}
}