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    #[serde(default)]
20    pub globals: Option<GlobalsSection>,
21}
22
23#[derive(Debug, Clone, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct GlobalsSection {
26    /// Globals removed from `_G` before user code runs. Named `block` because
27    /// it is one, next to the `allow` lists that read the other way round.
28    #[serde(default)]
29    pub block: Vec<String>,
30}
31
32#[derive(Debug, Clone, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct ModuleSection {
35    #[serde(default)]
36    pub allow: Option<Vec<String>>,
37}
38
39#[derive(Debug, Clone, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct EnvSection {
42    #[serde(default)]
43    pub allow: Vec<String>,
44}
45
46#[derive(Debug, Clone, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct HttpSection {
49    #[serde(default)]
50    pub max_response_bytes: Option<usize>,
51    #[serde(default)]
52    pub redact: Vec<String>,
53    #[serde(default)]
54    pub rules: Option<Vec<HttpRule>>,
55}
56
57#[derive(Debug, Clone, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct HttpRule {
60    pub hosts: Vec<String>,
61    #[serde(default)]
62    pub methods: Vec<String>,
63    #[serde(default)]
64    pub paths: Vec<String>,
65    #[serde(default)]
66    pub classify: Option<Classify>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
70#[serde(rename_all = "lowercase")]
71pub enum Classify {
72    Read,
73    Write,
74}
75
76impl PolicyFile {
77    pub fn parse(source: &str) -> Result<Self, String> {
78        let file: PolicyFile =
79            serde_yml::from_str(source).map_err(|e| format!("policy: invalid YAML: {e}"))?;
80        if file.version != SUPPORTED_VERSION {
81            return Err(format!(
82                "policy: unsupported version {} (this build understands {SUPPORTED_VERSION})",
83                file.version
84            ));
85        }
86        file.validate()?;
87        Ok(file)
88    }
89
90    fn validate(&self) -> Result<(), String> {
91        let Some(http) = &self.http else {
92            return Ok(());
93        };
94        for (i, rule) in http.rules.iter().flatten().enumerate() {
95            if rule.hosts.is_empty() {
96                return Err(format!("policy: http.rules[{i}] needs at least one host"));
97            }
98            for method in &rule.methods {
99                if !is_known_method(method) {
100                    return Err(format!(
101                        "policy: http.rules[{i}] has unknown method '{method}'"
102                    ));
103                }
104            }
105        }
106        Ok(())
107    }
108}
109
110fn is_known_method(method: &str) -> bool {
111    matches!(
112        method.trim().to_ascii_uppercase().as_str(),
113        "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
114    )
115}