Skip to main content

assay/lua/policy/
schema.rs

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