Skip to main content

cufflink_types/
policy.rs

1//! Names of the per-tenant platform policies and their modes, shared by the
2//! platform (which evaluates them) and the CLI (which sets them).
3
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum PolicyKey {
11    TenantBinding,
12    Graphql,
13    StorageScope,
14    Egress,
15    WasmLimits,
16    ServiceAccounts,
17    Audience,
18    RedisScope,
19    StrictFilters,
20}
21
22impl PolicyKey {
23    pub const ALL: [PolicyKey; 9] = [
24        PolicyKey::TenantBinding,
25        PolicyKey::Graphql,
26        PolicyKey::StorageScope,
27        PolicyKey::Egress,
28        PolicyKey::WasmLimits,
29        PolicyKey::ServiceAccounts,
30        PolicyKey::Audience,
31        PolicyKey::RedisScope,
32        PolicyKey::StrictFilters,
33    ];
34
35    pub fn as_str(self) -> &'static str {
36        match self {
37            PolicyKey::TenantBinding => "tenant_binding",
38            PolicyKey::Graphql => "graphql",
39            PolicyKey::StorageScope => "storage_scope",
40            PolicyKey::Egress => "egress",
41            PolicyKey::WasmLimits => "wasm_limits",
42            PolicyKey::ServiceAccounts => "service_accounts",
43            PolicyKey::Audience => "audience",
44            PolicyKey::RedisScope => "redis_scope",
45            PolicyKey::StrictFilters => "strict_filters",
46        }
47    }
48
49    /// The suffix of the `CUFFLINK_POLICY_<KEY>` environment variables.
50    pub fn env_suffix(self) -> String {
51        self.as_str().to_ascii_uppercase()
52    }
53}
54
55// `PolicyKey::ALL[i] as usize == i`: the platform indexes per-key tables by
56// the discriminant, so `ALL` must list the keys in declaration order.
57const _: () = {
58    let mut i = 0;
59    while i < PolicyKey::ALL.len() {
60        assert!(PolicyKey::ALL[i] as usize == i);
61        i += 1;
62    }
63};
64
65impl fmt::Display for PolicyKey {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.write_str(self.as_str())
68    }
69}
70
71impl FromStr for PolicyKey {
72    type Err = String;
73
74    fn from_str(s: &str) -> Result<Self, Self::Err> {
75        let wanted = s.trim();
76        PolicyKey::ALL
77            .into_iter()
78            .find(|key| key.as_str() == wanted)
79            .ok_or_else(|| {
80                let valid: Vec<&str> = PolicyKey::ALL.iter().map(|k| k.as_str()).collect();
81                format!(
82                    "unknown policy '{wanted}' (expected one of {})",
83                    valid.join(", ")
84                )
85            })
86    }
87}
88
89/// `Off` behaves as the platform did before the policy existed; `Warn`
90/// evaluates, records and allows; `Enforce` evaluates, records and denies.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
92#[serde(rename_all = "lowercase")]
93pub enum PolicyMode {
94    #[default]
95    Off,
96    Warn,
97    Enforce,
98}
99
100impl PolicyMode {
101    pub const ALL: [PolicyMode; 3] = [PolicyMode::Off, PolicyMode::Warn, PolicyMode::Enforce];
102
103    pub fn as_str(self) -> &'static str {
104        match self {
105            PolicyMode::Off => "off",
106            PolicyMode::Warn => "warn",
107            PolicyMode::Enforce => "enforce",
108        }
109    }
110}
111
112impl fmt::Display for PolicyMode {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(self.as_str())
115    }
116}
117
118impl FromStr for PolicyMode {
119    type Err = String;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        let wanted = s.trim().to_ascii_lowercase();
123        PolicyMode::ALL
124            .into_iter()
125            .find(|mode| mode.as_str() == wanted)
126            .ok_or_else(|| {
127                format!(
128                    "unknown policy mode '{}' (expected off, warn or enforce)",
129                    s.trim()
130                )
131            })
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn mode_and_key_round_trip_str() {
141        for key in PolicyKey::ALL {
142            assert_eq!(key.to_string().parse::<PolicyKey>(), Ok(key));
143            assert_eq!(
144                serde_json::to_value(key).expect("serialize"),
145                serde_json::json!(key.as_str())
146            );
147        }
148        for mode in PolicyMode::ALL {
149            assert_eq!(mode.to_string().parse::<PolicyMode>(), Ok(mode));
150        }
151        assert_eq!(" Enforce ".parse::<PolicyMode>(), Ok(PolicyMode::Enforce));
152        assert_eq!(PolicyKey::TenantBinding.env_suffix(), "TENANT_BINDING");
153    }
154
155    #[test]
156    fn unknown_names_list_the_valid_ones() {
157        let err = "tenant-binding".parse::<PolicyKey>().unwrap_err();
158        assert!(
159            err.contains("tenant_binding") && err.contains("strict_filters"),
160            "{err}"
161        );
162        let err = "loud".parse::<PolicyMode>().unwrap_err();
163        assert!(err.contains("enforce"), "{err}");
164    }
165}