1use serde::{Deserialize, Serialize};
7
8use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob};
9
10#[derive(Debug, Clone, PartialEq)]
15#[non_exhaustive]
16pub enum CompiledExpr {
17 True,
19 False,
21 And(Vec<CompiledExpr>),
23 Or(Vec<CompiledExpr>),
25 Not(Box<CompiledExpr>),
27
28 HasCapability(CanonicalCapability),
30 HasAllCapabilities(Vec<CanonicalCapability>),
32 HasAnyCapability(Vec<CanonicalCapability>),
34
35 IssuerIs(CanonicalDid),
37 IssuerIn(Vec<CanonicalDid>),
39 SubjectIs(CanonicalDid),
41 DelegatedBy(CanonicalDid),
43
44 NotRevoked,
46 NotExpired,
48 ExpiresAfter(i64),
50 IssuedWithin(i64),
52
53 RoleIs(String),
55 RoleIn(Vec<String>),
57
58 RepoIs(String),
60 RepoIn(Vec<String>),
62 RefMatches(ValidatedGlob),
64 PathAllowed(Vec<ValidatedGlob>),
66 EnvIs(String),
68 EnvIn(Vec<String>),
70
71 WorkloadIssuerIs(CanonicalDid),
73 WorkloadClaimEquals {
75 key: String,
77 value: String,
79 },
80
81 IsAgent,
83 IsHuman,
85 IsWorkload,
87
88 MaxChainDepth(u32),
90
91 AttrEquals {
93 key: String,
95 value: String,
97 },
98 AttrIn {
100 key: String,
102 values: Vec<String>,
104 },
105
106 MinAssurance(AssuranceLevel),
108 AssuranceLevelIs(AssuranceLevel),
110
111 ApprovalGate {
113 inner: Box<CompiledExpr>,
115 approvers: Vec<CanonicalDid>,
117 ttl_seconds: u64,
119 scope: ApprovalScope,
121 },
122}
123
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum ApprovalScope {
128 #[default]
130 Identity,
131 Scoped,
133 Full,
135}
136
137#[derive(Debug, Clone)]
141pub struct CompiledPolicy {
142 expr: CompiledExpr,
143 source_hash: [u8; 32],
144}
145
146impl CompiledPolicy {
147 pub(crate) fn new(expr: CompiledExpr, source_hash: [u8; 32]) -> Self {
151 Self { expr, source_hash }
152 }
153
154 pub fn expr(&self) -> &CompiledExpr {
156 &self.expr
157 }
158
159 pub fn source_hash(&self) -> &[u8; 32] {
162 &self.source_hash
163 }
164
165 pub fn describe(&self) -> String {
167 describe_expr(&self.expr, 0)
168 }
169}
170
171fn describe_expr(expr: &CompiledExpr, depth: usize) -> String {
172 let indent = " ".repeat(depth);
173 match expr {
174 CompiledExpr::True => format!("{indent}always allow"),
175 CompiledExpr::False => format!("{indent}always deny"),
176 CompiledExpr::And(children) => {
177 let parts: Vec<String> = children
178 .iter()
179 .map(|c| describe_expr(c, depth + 1))
180 .collect();
181 format!("{indent}ALL of:\n{}", parts.join("\n"))
182 }
183 CompiledExpr::Or(children) => {
184 let parts: Vec<String> = children
185 .iter()
186 .map(|c| describe_expr(c, depth + 1))
187 .collect();
188 format!("{indent}ANY of:\n{}", parts.join("\n"))
189 }
190 CompiledExpr::Not(inner) => format!("{indent}NOT:\n{}", describe_expr(inner, depth + 1)),
191 CompiledExpr::HasCapability(c) => format!("{indent}require capability: {c}"),
192 CompiledExpr::HasAllCapabilities(caps) => {
193 let names: Vec<String> = caps.iter().map(|c| c.to_string()).collect();
194 format!("{indent}require all capabilities: [{}]", names.join(", "))
195 }
196 CompiledExpr::HasAnyCapability(caps) => {
197 let names: Vec<String> = caps.iter().map(|c| c.to_string()).collect();
198 format!("{indent}require any capability: [{}]", names.join(", "))
199 }
200 CompiledExpr::IssuerIs(d) => format!("{indent}issuer must be: {d}"),
201 CompiledExpr::IssuerIn(ds) => {
202 let names: Vec<String> = ds.iter().map(|d| d.to_string()).collect();
203 format!("{indent}issuer in: [{}]", names.join(", "))
204 }
205 CompiledExpr::SubjectIs(d) => format!("{indent}subject must be: {d}"),
206 CompiledExpr::DelegatedBy(d) => format!("{indent}delegated by: {d}"),
207 CompiledExpr::NotRevoked => format!("{indent}not revoked"),
208 CompiledExpr::NotExpired => format!("{indent}not expired"),
209 CompiledExpr::ExpiresAfter(s) => format!("{indent}expires after {s}s"),
210 CompiledExpr::IssuedWithin(s) => format!("{indent}issued within {s}s"),
211 CompiledExpr::RoleIs(r) => format!("{indent}role must be: {r}"),
212 CompiledExpr::RoleIn(rs) => format!("{indent}role in: [{}]", rs.join(", ")),
213 CompiledExpr::RepoIs(r) => format!("{indent}repo must be: {r}"),
214 CompiledExpr::RepoIn(rs) => format!("{indent}repo in: [{}]", rs.join(", ")),
215 CompiledExpr::RefMatches(g) => format!("{indent}ref matches: {g}"),
216 CompiledExpr::PathAllowed(gs) => {
217 let names: Vec<String> = gs.iter().map(|g| g.to_string()).collect();
218 format!("{indent}paths allowed: [{}]", names.join(", "))
219 }
220 CompiledExpr::EnvIs(e) => format!("{indent}env must be: {e}"),
221 CompiledExpr::EnvIn(es) => format!("{indent}env in: [{}]", es.join(", ")),
222 CompiledExpr::WorkloadIssuerIs(d) => format!("{indent}workload issuer: {d}"),
223 CompiledExpr::WorkloadClaimEquals { key, value } => {
224 format!("{indent}workload claim {key} = {value}")
225 }
226 CompiledExpr::IsAgent => format!("{indent}signer is agent"),
227 CompiledExpr::IsHuman => format!("{indent}signer is human"),
228 CompiledExpr::IsWorkload => format!("{indent}signer is workload"),
229 CompiledExpr::MaxChainDepth(d) => format!("{indent}max chain depth: {d}"),
230 CompiledExpr::AttrEquals { key, value } => format!("{indent}attr {key} = {value}"),
231 CompiledExpr::AttrIn { key, values } => {
232 format!("{indent}attr {key} in: [{}]", values.join(", "))
233 }
234 CompiledExpr::MinAssurance(level) => {
235 format!("{indent}min assurance: {}", level.label())
236 }
237 CompiledExpr::AssuranceLevelIs(level) => {
238 format!("{indent}assurance must be: {}", level.label())
239 }
240 CompiledExpr::ApprovalGate {
241 approvers,
242 ttl_seconds,
243 ..
244 } => {
245 let names: Vec<String> = approvers.iter().map(|d| d.to_string()).collect();
246 format!(
247 "{indent}requires approval from [{}] (TTL: {ttl_seconds}s)",
248 names.join(", ")
249 )
250 }
251 }
252}
253
254impl PartialEq for CompiledPolicy {
255 fn eq(&self, other: &Self) -> bool {
256 self.expr == other.expr && self.source_hash == other.source_hash
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob};
264
265 #[test]
266 fn compiled_expr_true() {
267 let expr = CompiledExpr::True;
268 assert!(matches!(expr, CompiledExpr::True));
269 }
270
271 #[test]
272 fn compiled_expr_and() {
273 let expr = CompiledExpr::And(vec![CompiledExpr::True, CompiledExpr::False]);
274 match expr {
275 CompiledExpr::And(children) => assert_eq!(children.len(), 2),
276 _ => panic!("expected And"),
277 }
278 }
279
280 #[test]
281 fn compiled_expr_has_capability() {
282 let cap = CanonicalCapability::parse("sign_commit").unwrap();
283 let expr = CompiledExpr::HasCapability(cap.clone());
284 match expr {
285 CompiledExpr::HasCapability(c) => assert_eq!(c, cap),
286 _ => panic!("expected HasCapability"),
287 }
288 }
289
290 #[test]
291 fn compiled_expr_issuer_is() {
292 let did = CanonicalDid::parse("did:keri:EOrg123").unwrap();
293 let expr = CompiledExpr::IssuerIs(did.clone());
294 match expr {
295 CompiledExpr::IssuerIs(d) => assert_eq!(d, did),
296 _ => panic!("expected IssuerIs"),
297 }
298 }
299
300 #[test]
301 fn compiled_expr_ref_matches() {
302 let glob = ValidatedGlob::parse("refs/heads/*").unwrap();
303 let expr = CompiledExpr::RefMatches(glob.clone());
304 match expr {
305 CompiledExpr::RefMatches(g) => assert_eq!(g, glob),
306 _ => panic!("expected RefMatches"),
307 }
308 }
309
310 #[test]
311 fn compiled_policy_accessors() {
312 let expr = CompiledExpr::True;
313 let hash = [42u8; 32];
314 let policy = CompiledPolicy::new(expr.clone(), hash);
315
316 assert_eq!(*policy.expr(), expr);
317 assert_eq!(*policy.source_hash(), hash);
318 }
319
320 #[test]
321 fn compiled_policy_equality() {
322 let expr1 = CompiledExpr::True;
323 let expr2 = CompiledExpr::True;
324 let hash = [42u8; 32];
325
326 let policy1 = CompiledPolicy::new(expr1, hash);
327 let policy2 = CompiledPolicy::new(expr2, hash);
328
329 assert_eq!(policy1, policy2);
330 }
331
332 #[test]
333 fn compiled_policy_inequality_different_hash() {
334 let expr = CompiledExpr::True;
335 let hash1 = [42u8; 32];
336 let hash2 = [43u8; 32];
337
338 let policy1 = CompiledPolicy::new(expr.clone(), hash1);
339 let policy2 = CompiledPolicy::new(expr, hash2);
340
341 assert_ne!(policy1, policy2);
342 }
343}