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