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