Skip to main content

assay/lua/policy/
schema.rs

1use 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}