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
64const _: () = {
65    let mut i = 0;
66    while i < PolicyKey::ALL.len() {
67        assert!(PolicyKey::ALL[i] as usize == i);
68        i += 1;
69    }
70};
71
72impl fmt::Display for PolicyKey {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(self.as_str())
75    }
76}
77
78impl FromStr for PolicyKey {
79    type Err = String;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        let wanted = s.trim();
83        PolicyKey::ALL
84            .into_iter()
85            .find(|key| key.as_str() == wanted)
86            .ok_or_else(|| {
87                let valid: Vec<&str> = PolicyKey::ALL.iter().map(|k| k.as_str()).collect();
88                format!(
89                    "unknown policy '{wanted}' (expected one of {})",
90                    valid.join(", ")
91                )
92            })
93    }
94}
95
96/// `Off` behaves as the platform did before the policy existed; `Warn`
97/// evaluates, records and allows; `Enforce` evaluates, records and denies.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum PolicyMode {
101    #[default]
102    Off,
103    Warn,
104    Enforce,
105}
106
107impl PolicyMode {
108    pub const ALL: [PolicyMode; 3] = [PolicyMode::Off, PolicyMode::Warn, PolicyMode::Enforce];
109
110    pub fn as_str(self) -> &'static str {
111        match self {
112            PolicyMode::Off => "off",
113            PolicyMode::Warn => "warn",
114            PolicyMode::Enforce => "enforce",
115        }
116    }
117}
118
119impl fmt::Display for PolicyMode {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        f.write_str(self.as_str())
122    }
123}
124
125impl FromStr for PolicyMode {
126    type Err = String;
127
128    fn from_str(s: &str) -> Result<Self, Self::Err> {
129        let wanted = s.trim().to_ascii_lowercase();
130        PolicyMode::ALL
131            .into_iter()
132            .find(|mode| mode.as_str() == wanted)
133            .ok_or_else(|| {
134                format!(
135                    "unknown policy mode '{}' (expected off, warn or enforce)",
136                    s.trim()
137                )
138            })
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn mode_and_key_round_trip_str() {
148        for key in PolicyKey::ALL {
149            assert_eq!(key.to_string().parse::<PolicyKey>(), Ok(key));
150            assert_eq!(
151                serde_json::to_value(key).expect("serialize"),
152                serde_json::json!(key.as_str())
153            );
154        }
155        for mode in PolicyMode::ALL {
156            assert_eq!(mode.to_string().parse::<PolicyMode>(), Ok(mode));
157        }
158        assert_eq!(" Enforce ".parse::<PolicyMode>(), Ok(PolicyMode::Enforce));
159        assert_eq!(PolicyKey::TenantBinding.env_suffix(), "TENANT_BINDING");
160    }
161
162    #[test]
163    fn unknown_names_list_the_valid_ones() {
164        let err = "tenant-binding".parse::<PolicyKey>().unwrap_err();
165        assert!(
166            err.contains("tenant_binding") && err.contains("strict_filters"),
167            "{err}"
168        );
169        let err = "loud".parse::<PolicyMode>().unwrap_err();
170        assert!(err.contains("enforce"), "{err}");
171    }
172}