aa_security/policy/error.rs
1//! Errors raised while parsing the canonical policy AST.
2
3use std::fmt;
4
5/// Errors that can occur while parsing a [`PolicyDocument`](super::PolicyDocument).
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum PolicyParseError {
8 /// The YAML could not be deserialized.
9 Yaml(String),
10 /// A capability token was not recognised.
11 InvalidCapability {
12 /// The offending raw token.
13 raw: String,
14 /// Why it was rejected.
15 reason: String,
16 },
17 /// A syscall name in the `syscalls.allow` list was not recognised.
18 InvalidSyscall {
19 /// The offending raw token.
20 raw: String,
21 /// Why it was rejected.
22 reason: String,
23 },
24 /// A structural key was not part of the known policy schema.
25 ///
26 /// Raised when a security-relevant section or field is misspelled (e.g.
27 /// `dney:` for `deny:`, `allow_list:` for `allowlist:`). Such typos would
28 /// otherwise be silently dropped by `#[serde(flatten)]` — yielding an empty,
29 /// permissive policy that parses successfully. Surfacing them keeps policy
30 /// parsing fail-closed (AAASM-3874).
31 UnknownKey {
32 /// Dotted path to the mapping that contained the unknown key.
33 path: String,
34 /// The unrecognised key.
35 key: String,
36 },
37 /// The document was empty or null.
38 ///
39 /// An empty / null / `{}` policy deserializes to an all-`None` document,
40 /// which is fully permissive (no capability denials, no allowlist, no tool
41 /// gating). A policy must positively declare its posture, so a blank
42 /// document is rejected rather than silently defaulting open (AAASM-3997).
43 EmptyDocument,
44 /// The document parsed but declared no enforcement dimension.
45 ///
46 /// A metadata-only document (e.g. just `apiVersion`/`kind`/`metadata`, with
47 /// no `network`, `capabilities`, `tools`, or `syscalls`) deserializes to a
48 /// fully-permissive policy exactly like an empty one. It is rejected so a
49 /// policy cannot become open by omission rather than by declaration
50 /// (AAASM-4020, extending the AAASM-3997 empty/null floor).
51 NoEnforcementSection,
52}
53
54impl fmt::Display for PolicyParseError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Yaml(msg) => write!(f, "policy YAML parse error: {msg}"),
58 Self::InvalidCapability { raw, reason } => {
59 write!(f, "invalid capability {raw:?}: {reason}")
60 }
61 Self::InvalidSyscall { raw, reason } => {
62 write!(f, "invalid syscall {raw:?}: {reason}")
63 }
64 Self::UnknownKey { path, key } => {
65 write!(f, "unknown policy key {key:?} under {path}")
66 }
67 Self::EmptyDocument => {
68 write!(
69 f,
70 "empty or null policy document is not permitted (would be fully permissive)"
71 )
72 }
73 Self::NoEnforcementSection => {
74 write!(
75 f,
76 "policy declares no enforcement section (network/capabilities/tools/syscalls); \
77 it would be fully permissive"
78 )
79 }
80 }
81 }
82}
83
84impl std::error::Error for PolicyParseError {}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn display_yaml_error() {
92 let err = PolicyParseError::Yaml("bad".to_string());
93 assert_eq!(err.to_string(), "policy YAML parse error: bad");
94 }
95
96 #[test]
97 fn display_invalid_capability() {
98 let err = PolicyParseError::InvalidCapability {
99 raw: "teleport".to_string(),
100 reason: "unknown capability: 'teleport'".to_string(),
101 };
102 assert!(err.to_string().contains("teleport"));
103 }
104
105 #[test]
106 fn display_unknown_key() {
107 let err = PolicyParseError::UnknownKey {
108 path: "capabilities".to_string(),
109 key: "dney".to_string(),
110 };
111 assert_eq!(err.to_string(), "unknown policy key \"dney\" under capabilities");
112 }
113}