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