assay/lua/policy/
schema.rs1use serde::Deserialize;
2
3pub const SUPPORTED_VERSION: u32 = 1;
4
5#[derive(Debug, Clone, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct PolicyFile {
8 pub version: u32,
9 #[serde(default)]
10 pub modules: Option<ModuleSection>,
11 #[serde(default)]
12 pub env: Option<EnvSection>,
13 #[serde(default)]
14 pub http: Option<HttpSection>,
15}
16
17#[derive(Debug, Clone, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct ModuleSection {
20 #[serde(default)]
21 pub allow: Option<Vec<String>>,
22}
23
24#[derive(Debug, Clone, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct EnvSection {
27 #[serde(default)]
28 pub allow: Vec<String>,
29}
30
31#[derive(Debug, Clone, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct HttpSection {
34 #[serde(default)]
35 pub max_response_bytes: Option<usize>,
36 #[serde(default)]
37 pub redact: Vec<String>,
38 #[serde(default)]
39 pub rules: Option<Vec<HttpRule>>,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct HttpRule {
45 pub hosts: Vec<String>,
46 #[serde(default)]
47 pub methods: Vec<String>,
48 #[serde(default)]
49 pub paths: Vec<String>,
50 #[serde(default)]
51 pub classify: Option<Classify>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum Classify {
57 Read,
58 Write,
59}
60
61impl PolicyFile {
62 pub fn parse(source: &str) -> Result<Self, String> {
63 let file: PolicyFile =
64 serde_yml::from_str(source).map_err(|e| format!("policy: invalid YAML: {e}"))?;
65 if file.version != SUPPORTED_VERSION {
66 return Err(format!(
67 "policy: unsupported version {} (this build understands {SUPPORTED_VERSION})",
68 file.version
69 ));
70 }
71 file.validate()?;
72 Ok(file)
73 }
74
75 fn validate(&self) -> Result<(), String> {
76 let Some(http) = &self.http else {
77 return Ok(());
78 };
79 for (i, rule) in http.rules.iter().flatten().enumerate() {
80 if rule.hosts.is_empty() {
81 return Err(format!("policy: http.rules[{i}] needs at least one host"));
82 }
83 for method in &rule.methods {
84 if !is_known_method(method) {
85 return Err(format!(
86 "policy: http.rules[{i}] has unknown method '{method}'"
87 ));
88 }
89 }
90 }
91 Ok(())
92 }
93}
94
95fn is_known_method(method: &str) -> bool {
96 matches!(
97 method.trim().to_ascii_uppercase().as_str(),
98 "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
99 )
100}