Skip to main content

auths_policy/
compile.rs

1//! Compile `Expr` to `CompiledPolicy`.
2//!
3//! One-shot validation. Every string parsed, every constraint checked,
4//! recursion bounded.
5
6use crate::compiled::{CompiledExpr, CompiledPolicy};
7use crate::expr::Expr;
8use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob};
9
10/// Maximum length for attribute keys.
11const MAX_ATTR_KEY_LEN: usize = 64;
12
13/// Maximum allowed value for `MaxChainDepth` expressions.
14pub const MAX_CHAIN_DEPTH_LIMIT: u32 = 16;
15
16/// Hard limits enforced at compile time.
17///
18/// These are not configurable — they're safety bounds.
19/// Any policy exceeding them is rejected. The numbers are
20/// generous for legitimate use and tight for abuse.
21#[non_exhaustive]
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PolicyLimits {
24    /// Maximum recursion depth for nested expressions.
25    pub max_depth: u32,
26    /// Maximum total number of AST nodes.
27    pub max_total_nodes: u32,
28    /// Maximum items in any single list (And, Or, IssuerIn, etc.).
29    pub max_list_items: usize,
30    /// Maximum size of policy JSON before deserialization.
31    pub max_json_bytes: usize,
32    /// Maximum allowed value for `MaxChainDepth` expressions.
33    pub max_chain_depth_value: u32,
34}
35
36impl Default for PolicyLimits {
37    fn default() -> Self {
38        Self {
39            max_depth: 64,
40            max_total_nodes: 1024,
41            max_list_items: 256,
42            max_json_bytes: 64 * 1024, // 64 KB
43            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
44        }
45    }
46}
47
48/// Error that occurred during policy compilation.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct CompileError {
51    /// Path to the error in the expression tree (e.g., "root.and[0].issuer_is").
52    pub path: String,
53    /// Human-readable error message.
54    pub message: String,
55}
56
57impl std::fmt::Display for CompileError {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "at {}: {}", self.path, self.message)
60    }
61}
62
63impl std::error::Error for CompileError {}
64
65/// Compile a serializable `Expr` into a validated `CompiledPolicy`.
66///
67/// Uses default `PolicyLimits`. For custom limits, use `compile_with_limits`.
68///
69/// Fails eagerly on:
70/// - Invalid DIDs, capabilities, or glob patterns
71/// - Recursion depth exceeding limits
72/// - Total nodes exceeding limits
73/// - List items exceeding limits
74/// - Invalid attribute keys
75/// - Empty `And`/`Or` children (ambiguous semantics)
76///
77/// # Errors
78///
79/// Returns a vector of all compilation errors found.
80pub fn compile(expr: &Expr) -> Result<CompiledPolicy, Vec<CompileError>> {
81    compile_with_limits(expr, &PolicyLimits::default())
82}
83
84/// Compile from raw JSON bytes with size check.
85///
86/// Rejects input larger than `PolicyLimits::max_json_bytes` before
87/// attempting deserialization. This prevents allocation bombs from
88/// untrusted policy files.
89///
90/// # Errors
91///
92/// Returns a vector of all compilation errors found.
93pub fn compile_from_json(json: &[u8]) -> Result<CompiledPolicy, Vec<CompileError>> {
94    compile_from_json_with_limits(json, &PolicyLimits::default())
95}
96
97/// Compile from raw JSON bytes with custom limits.
98///
99/// # Errors
100///
101/// Returns a vector of all compilation errors found.
102pub fn compile_from_json_with_limits(
103    json: &[u8],
104    limits: &PolicyLimits,
105) -> Result<CompiledPolicy, Vec<CompileError>> {
106    if json.len() > limits.max_json_bytes {
107        return Err(vec![CompileError {
108            path: "root".into(),
109            message: format!(
110                "policy JSON is {} bytes, max {} bytes",
111                json.len(),
112                limits.max_json_bytes
113            ),
114        }]);
115    }
116
117    let expr: Expr = serde_json::from_slice(json).map_err(|e| {
118        vec![CompileError {
119            path: "root".into(),
120            message: format!("invalid JSON: {}", e),
121        }]
122    })?;
123
124    compile_with_limits(&expr, limits)
125}
126
127/// Compile with custom limits.
128///
129/// # Errors
130///
131/// Returns a vector of all compilation errors found.
132pub fn compile_with_limits(
133    expr: &Expr,
134    limits: &PolicyLimits,
135) -> Result<CompiledPolicy, Vec<CompileError>> {
136    let mut errors = Vec::new();
137    let mut node_count: u32 = 0;
138    let compiled = compile_inner(expr, "root", 0, limits, &mut node_count, &mut errors);
139
140    if errors.is_empty() {
141        let source_json = serde_json::to_vec(expr).unwrap_or_default();
142        let source_hash = compute_hash(&source_json);
143        Ok(CompiledPolicy::new(compiled, source_hash))
144    } else {
145        Err(errors)
146    }
147}
148
149fn compute_hash(data: &[u8]) -> [u8; 32] {
150    *blake3::hash(data).as_bytes()
151}
152
153fn compile_inner(
154    expr: &Expr,
155    path: &str,
156    depth: u32,
157    limits: &PolicyLimits,
158    node_count: &mut u32,
159    errors: &mut Vec<CompileError>,
160) -> CompiledExpr {
161    // Check depth limit
162    if depth > limits.max_depth {
163        errors.push(CompileError {
164            path: path.to_string(),
165            message: format!("recursion depth exceeds {}", limits.max_depth),
166        });
167        return CompiledExpr::False;
168    }
169
170    // Increment and check total node count
171    *node_count += 1;
172    if *node_count > limits.max_total_nodes {
173        errors.push(CompileError {
174            path: path.to_string(),
175            message: format!("total nodes exceed {}", limits.max_total_nodes),
176        });
177        return CompiledExpr::False;
178    }
179
180    match expr {
181        Expr::True => CompiledExpr::True,
182        Expr::False => CompiledExpr::False,
183
184        Expr::And(children) => {
185            if children.is_empty() {
186                errors.push(CompileError {
187                    path: path.into(),
188                    message: "And with no children is ambiguous".into(),
189                });
190            }
191            check_list_items(children.len(), limits, path, errors);
192            let compiled: Vec<_> = children
193                .iter()
194                .enumerate()
195                .map(|(i, c)| {
196                    compile_inner(
197                        c,
198                        &format!("{}.and[{}]", path, i),
199                        depth + 1,
200                        limits,
201                        node_count,
202                        errors,
203                    )
204                })
205                .collect();
206            CompiledExpr::And(compiled)
207        }
208
209        Expr::Or(children) => {
210            if children.is_empty() {
211                errors.push(CompileError {
212                    path: path.into(),
213                    message: "Or with no children is ambiguous".into(),
214                });
215            }
216            check_list_items(children.len(), limits, path, errors);
217            let compiled: Vec<_> = children
218                .iter()
219                .enumerate()
220                .map(|(i, c)| {
221                    compile_inner(
222                        c,
223                        &format!("{}.or[{}]", path, i),
224                        depth + 1,
225                        limits,
226                        node_count,
227                        errors,
228                    )
229                })
230                .collect();
231            CompiledExpr::Or(compiled)
232        }
233
234        Expr::Not(inner) => {
235            if matches!(inner.as_ref(), Expr::ApprovalGate { .. }) {
236                errors.push(CompileError {
237                    path: path.into(),
238                    message: "Not cannot wrap an ApprovalGate expression".into(),
239                });
240            }
241            let compiled = compile_inner(
242                inner,
243                &format!("{}.not", path),
244                depth + 1,
245                limits,
246                node_count,
247                errors,
248            );
249            CompiledExpr::Not(Box::new(compiled))
250        }
251
252        Expr::HasCapability(s) => match CanonicalCapability::parse(s) {
253            Ok(cap) => CompiledExpr::HasCapability(cap),
254            Err(e) => {
255                errors.push(CompileError {
256                    path: path.into(),
257                    message: e.to_string(),
258                });
259                CompiledExpr::False
260            }
261        },
262
263        Expr::HasAllCapabilities(caps) => {
264            check_list_items(caps.len(), limits, path, errors);
265            let compiled: Vec<_> = caps
266                .iter()
267                .filter_map(|s| match CanonicalCapability::parse(s) {
268                    Ok(c) => Some(c),
269                    Err(e) => {
270                        errors.push(CompileError {
271                            path: path.into(),
272                            message: e.to_string(),
273                        });
274                        None
275                    }
276                })
277                .collect();
278            CompiledExpr::HasAllCapabilities(compiled)
279        }
280
281        Expr::HasAnyCapability(caps) => {
282            check_list_items(caps.len(), limits, path, errors);
283            let compiled: Vec<_> = caps
284                .iter()
285                .filter_map(|s| match CanonicalCapability::parse(s) {
286                    Ok(c) => Some(c),
287                    Err(e) => {
288                        errors.push(CompileError {
289                            path: path.into(),
290                            message: e.to_string(),
291                        });
292                        None
293                    }
294                })
295                .collect();
296            CompiledExpr::HasAnyCapability(compiled)
297        }
298
299        Expr::IssuerIs(s) => match CanonicalDid::parse(s) {
300            Ok(did) => CompiledExpr::IssuerIs(did),
301            Err(e) => {
302                errors.push(CompileError {
303                    path: path.into(),
304                    message: e.to_string(),
305                });
306                CompiledExpr::False
307            }
308        },
309
310        Expr::IssuerIn(dids) => {
311            check_list_items(dids.len(), limits, path, errors);
312            let compiled: Vec<_> = dids
313                .iter()
314                .filter_map(|s| match CanonicalDid::parse(s) {
315                    Ok(d) => Some(d),
316                    Err(e) => {
317                        errors.push(CompileError {
318                            path: path.into(),
319                            message: e.to_string(),
320                        });
321                        None
322                    }
323                })
324                .collect();
325            CompiledExpr::IssuerIn(compiled)
326        }
327
328        Expr::SubjectIs(s) => match CanonicalDid::parse(s) {
329            Ok(did) => CompiledExpr::SubjectIs(did),
330            Err(e) => {
331                errors.push(CompileError {
332                    path: path.into(),
333                    message: e.to_string(),
334                });
335                CompiledExpr::False
336            }
337        },
338
339        Expr::DelegatedBy(s) => match CanonicalDid::parse(s) {
340            Ok(did) => CompiledExpr::DelegatedBy(did),
341            Err(e) => {
342                errors.push(CompileError {
343                    path: path.into(),
344                    message: e.to_string(),
345                });
346                CompiledExpr::False
347            }
348        },
349
350        Expr::RefMatches(s) => match ValidatedGlob::parse(s) {
351            Ok(g) => CompiledExpr::RefMatches(g),
352            Err(e) => {
353                errors.push(CompileError {
354                    path: path.into(),
355                    message: e.to_string(),
356                });
357                CompiledExpr::False
358            }
359        },
360
361        Expr::PathAllowed(patterns) => {
362            check_list_items(patterns.len(), limits, path, errors);
363            let compiled: Vec<_> = patterns
364                .iter()
365                .filter_map(|s| match ValidatedGlob::parse(s) {
366                    Ok(g) => Some(g),
367                    Err(e) => {
368                        errors.push(CompileError {
369                            path: path.into(),
370                            message: e.to_string(),
371                        });
372                        None
373                    }
374                })
375                .collect();
376            CompiledExpr::PathAllowed(compiled)
377        }
378
379        Expr::AttrEquals { key, value } => {
380            validate_attr_key(key, path, errors);
381            CompiledExpr::AttrEquals {
382                key: key.clone(),
383                value: value.clone(),
384            }
385        }
386
387        Expr::AttrIn { key, values } => {
388            validate_attr_key(key, path, errors);
389            check_list_items(values.len(), limits, path, errors);
390            CompiledExpr::AttrIn {
391                key: key.clone(),
392                values: values.clone(),
393            }
394        }
395
396        Expr::WorkloadIssuerIs(s) => match CanonicalDid::parse(s) {
397            Ok(did) => CompiledExpr::WorkloadIssuerIs(did),
398            Err(e) => {
399                errors.push(CompileError {
400                    path: path.into(),
401                    message: e.to_string(),
402                });
403                CompiledExpr::False
404            }
405        },
406
407        Expr::WorkloadClaimEquals { key, value } => {
408            validate_attr_key(key, path, errors);
409            CompiledExpr::WorkloadClaimEquals {
410                key: key.clone(),
411                value: value.clone(),
412            }
413        }
414
415        // Pass-through variants (no string validation needed)
416        Expr::NotRevoked => CompiledExpr::NotRevoked,
417        Expr::NotExpired => CompiledExpr::NotExpired,
418        Expr::ExpiresAfter(s) => CompiledExpr::ExpiresAfter(*s),
419        Expr::IssuedWithin(s) => CompiledExpr::IssuedWithin(*s),
420        Expr::RoleIs(s) => CompiledExpr::RoleIs(s.clone()),
421        Expr::RoleIn(v) => {
422            check_list_items(v.len(), limits, path, errors);
423            CompiledExpr::RoleIn(v.clone())
424        }
425        Expr::RepoIs(s) => CompiledExpr::RepoIs(s.clone()),
426        Expr::RepoIn(v) => {
427            check_list_items(v.len(), limits, path, errors);
428            CompiledExpr::RepoIn(v.clone())
429        }
430        Expr::EnvIs(s) => CompiledExpr::EnvIs(s.clone()),
431        Expr::EnvIn(v) => {
432            check_list_items(v.len(), limits, path, errors);
433            CompiledExpr::EnvIn(v.clone())
434        }
435        // Pass-through signer type variants (no validation needed)
436        Expr::IsAgent => CompiledExpr::IsAgent,
437        Expr::IsHuman => CompiledExpr::IsHuman,
438        Expr::IsWorkload => CompiledExpr::IsWorkload,
439
440        Expr::MaxChainDepth(d) => {
441            if *d > limits.max_chain_depth_value {
442                errors.push(CompileError {
443                    path: path.into(),
444                    message: format!(
445                        "MaxChainDepth({}) exceeds limit of {}",
446                        d, limits.max_chain_depth_value
447                    ),
448                });
449            }
450            CompiledExpr::MaxChainDepth(*d)
451        }
452
453        Expr::MinAssurance(s) => match s.parse::<AssuranceLevel>() {
454            Ok(level) => CompiledExpr::MinAssurance(level),
455            Err(_) => {
456                errors.push(CompileError {
457                    path: path.into(),
458                    message: format!(
459                        "invalid assurance level '{}': expected one of: sovereign, authenticated, token_verified, self_asserted",
460                        s
461                    ),
462                });
463                CompiledExpr::False
464            }
465        },
466
467        Expr::AssuranceLevelIs(s) => match s.parse::<AssuranceLevel>() {
468            Ok(level) => CompiledExpr::AssuranceLevelIs(level),
469            Err(_) => {
470                errors.push(CompileError {
471                    path: path.into(),
472                    message: format!(
473                        "invalid assurance level '{}': expected one of: sovereign, authenticated, token_verified, self_asserted",
474                        s
475                    ),
476                });
477                CompiledExpr::False
478            }
479        },
480
481        Expr::ApprovalGate {
482            inner,
483            approvers,
484            ttl_seconds,
485            scope,
486        } => {
487            if matches!(inner.as_ref(), Expr::Not(_)) {
488                errors.push(CompileError {
489                    path: path.into(),
490                    message: "ApprovalGate cannot wrap a Not expression".into(),
491                });
492            }
493            let compiled_inner = compile_inner(
494                inner,
495                &format!("{}.approval_gate", path),
496                depth + 1,
497                limits,
498                node_count,
499                errors,
500            );
501            let compiled_approvers: Vec<_> = approvers
502                .iter()
503                .filter_map(|s| match CanonicalDid::parse(s) {
504                    Ok(d) => Some(d),
505                    Err(e) => {
506                        errors.push(CompileError {
507                            path: path.into(),
508                            message: e.to_string(),
509                        });
510                        None
511                    }
512                })
513                .collect();
514            let compiled_scope = match scope.as_deref() {
515                Some("identity") | None => crate::compiled::ApprovalScope::Identity,
516                Some("scoped") => crate::compiled::ApprovalScope::Scoped,
517                Some("full") => crate::compiled::ApprovalScope::Full,
518                Some(other) => {
519                    errors.push(CompileError {
520                        path: path.into(),
521                        message: format!(
522                            "invalid approval scope '{}', expected 'identity', 'scoped', or 'full'",
523                            other
524                        ),
525                    });
526                    crate::compiled::ApprovalScope::Identity
527                }
528            };
529            CompiledExpr::ApprovalGate {
530                inner: Box::new(compiled_inner),
531                approvers: compiled_approvers,
532                ttl_seconds: *ttl_seconds,
533                scope: compiled_scope,
534            }
535        }
536    }
537}
538
539fn check_list_items(len: usize, limits: &PolicyLimits, path: &str, errors: &mut Vec<CompileError>) {
540    if len > limits.max_list_items {
541        errors.push(CompileError {
542            path: path.into(),
543            message: format!("list has {} items, max {}", len, limits.max_list_items),
544        });
545    }
546}
547
548fn validate_attr_key(key: &str, path: &str, errors: &mut Vec<CompileError>) {
549    if key.is_empty() || key.len() > MAX_ATTR_KEY_LEN {
550        errors.push(CompileError {
551            path: path.into(),
552            message: format!("attr key must be 1-{} chars", MAX_ATTR_KEY_LEN),
553        });
554    }
555    if !key.chars().all(|c| c.is_alphanumeric() || c == '_') {
556        errors.push(CompileError {
557            path: path.into(),
558            message: format!(
559                "attr key '{}' contains invalid chars (alphanum + _ only)",
560                key
561            ),
562        });
563    }
564    if key.contains('.') || key.contains('/') {
565        errors.push(CompileError {
566            path: path.into(),
567            message: "attr keys must not contain dot-paths or slashes".into(),
568        });
569    }
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn compile_true() {
578        let expr = Expr::True;
579        let policy = compile(&expr).unwrap();
580        assert!(matches!(policy.expr(), CompiledExpr::True));
581    }
582
583    #[test]
584    fn compile_false() {
585        let expr = Expr::False;
586        let policy = compile(&expr).unwrap();
587        assert!(matches!(policy.expr(), CompiledExpr::False));
588    }
589
590    #[test]
591    fn compile_has_capability_valid() {
592        let expr = Expr::HasCapability("sign_commit".into());
593        let policy = compile(&expr).unwrap();
594        match policy.expr() {
595            CompiledExpr::HasCapability(cap) => assert_eq!(cap.as_str(), "sign_commit"),
596            _ => panic!("expected HasCapability"),
597        }
598    }
599
600    #[test]
601    fn compile_has_capability_invalid() {
602        let expr = Expr::HasCapability("invalid capability!".into());
603        let errors = compile(&expr).unwrap_err();
604        assert!(!errors.is_empty());
605    }
606
607    #[test]
608    fn compile_issuer_is_valid() {
609        let expr = Expr::IssuerIs("did:keri:EOrg123".into());
610        let policy = compile(&expr).unwrap();
611        match policy.expr() {
612            CompiledExpr::IssuerIs(did) => assert_eq!(did.as_str(), "did:keri:EOrg123"),
613            _ => panic!("expected IssuerIs"),
614        }
615    }
616
617    #[test]
618    fn compile_issuer_is_invalid() {
619        let expr = Expr::IssuerIs("not-a-did".into());
620        let errors = compile(&expr).unwrap_err();
621        assert!(!errors.is_empty());
622    }
623
624    #[test]
625    fn compile_ref_matches_valid() {
626        let expr = Expr::RefMatches("refs/heads/*".into());
627        let policy = compile(&expr).unwrap();
628        match policy.expr() {
629            CompiledExpr::RefMatches(g) => assert_eq!(g.as_str(), "refs/heads/*"),
630            _ => panic!("expected RefMatches"),
631        }
632    }
633
634    #[test]
635    fn compile_ref_matches_path_traversal() {
636        let expr = Expr::RefMatches("refs/../secrets".into());
637        let errors = compile(&expr).unwrap_err();
638        assert!(!errors.is_empty());
639        assert!(errors[0].message.contains("path traversal"));
640    }
641
642    #[test]
643    fn compile_and_valid() {
644        let expr = Expr::And(vec![Expr::NotRevoked, Expr::NotExpired]);
645        let policy = compile(&expr).unwrap();
646        match policy.expr() {
647            CompiledExpr::And(children) => assert_eq!(children.len(), 2),
648            _ => panic!("expected And"),
649        }
650    }
651
652    #[test]
653    fn compile_and_empty() {
654        let expr = Expr::And(vec![]);
655        let errors = compile(&expr).unwrap_err();
656        assert!(errors.iter().any(|e| e.message.contains("ambiguous")));
657    }
658
659    #[test]
660    fn compile_or_empty() {
661        let expr = Expr::Or(vec![]);
662        let errors = compile(&expr).unwrap_err();
663        assert!(errors.iter().any(|e| e.message.contains("ambiguous")));
664    }
665
666    #[test]
667    fn compile_not() {
668        let expr = Expr::Not(Box::new(Expr::True));
669        let policy = compile(&expr).unwrap();
670        match policy.expr() {
671            CompiledExpr::Not(inner) => assert!(matches!(**inner, CompiledExpr::True)),
672            _ => panic!("expected Not"),
673        }
674    }
675
676    #[test]
677    fn compile_nested() {
678        let expr = Expr::And(vec![
679            Expr::NotRevoked,
680            Expr::Or(vec![
681                Expr::HasCapability("admin".into()),
682                Expr::HasCapability("write".into()),
683            ]),
684        ]);
685        let policy = compile(&expr).unwrap();
686        match policy.expr() {
687            CompiledExpr::And(children) => {
688                assert_eq!(children.len(), 2);
689                assert!(matches!(children[1], CompiledExpr::Or(_)));
690            }
691            _ => panic!("expected And"),
692        }
693    }
694
695    #[test]
696    fn compile_depth_exceeded() {
697        // Create deeply nested expression
698        let mut expr = Expr::True;
699        for _ in 0..100 {
700            expr = Expr::Not(Box::new(expr));
701        }
702        let errors = compile(&expr).unwrap_err();
703        assert!(errors.iter().any(|e| e.message.contains("recursion depth")));
704    }
705
706    #[test]
707    fn compile_attr_equals_valid() {
708        let expr = Expr::AttrEquals {
709            key: "team".into(),
710            value: "platform".into(),
711        };
712        let policy = compile(&expr).unwrap();
713        match policy.expr() {
714            CompiledExpr::AttrEquals { key, value } => {
715                assert_eq!(key, "team");
716                assert_eq!(value, "platform");
717            }
718            _ => panic!("expected AttrEquals"),
719        }
720    }
721
722    #[test]
723    fn compile_attr_equals_invalid_key() {
724        let expr = Expr::AttrEquals {
725            key: "invalid.key".into(),
726            value: "value".into(),
727        };
728        let errors = compile(&expr).unwrap_err();
729        assert!(!errors.is_empty());
730    }
731
732    #[test]
733    fn compile_multiple_errors() {
734        let expr = Expr::And(vec![
735            Expr::IssuerIs("bad-did".into()),
736            Expr::HasCapability("bad cap!".into()),
737        ]);
738        let errors = compile(&expr).unwrap_err();
739        assert!(errors.len() >= 2);
740    }
741
742    #[test]
743    fn policy_has_source_hash() {
744        let expr = Expr::True;
745        let policy = compile(&expr).unwrap();
746        let hash = policy.source_hash();
747        // Hash should be non-zero
748        assert!(hash.iter().any(|&b| b != 0));
749    }
750
751    #[test]
752    fn same_expr_same_hash() {
753        let expr1 = Expr::HasCapability("sign_commit".into());
754        let expr2 = Expr::HasCapability("sign_commit".into());
755
756        let policy1 = compile(&expr1).unwrap();
757        let policy2 = compile(&expr2).unwrap();
758
759        assert_eq!(policy1.source_hash(), policy2.source_hash());
760    }
761
762    #[test]
763    fn different_expr_different_hash() {
764        let expr1 = Expr::HasCapability("sign_commit".into());
765        let expr2 = Expr::HasCapability("read".into());
766
767        let policy1 = compile(&expr1).unwrap();
768        let policy2 = compile(&expr2).unwrap();
769
770        assert_ne!(policy1.source_hash(), policy2.source_hash());
771    }
772
773    // Complexity bounds tests
774
775    #[test]
776    fn policy_limits_default() {
777        let limits = PolicyLimits::default();
778        assert_eq!(limits.max_depth, 64);
779        assert_eq!(limits.max_total_nodes, 1024);
780        assert_eq!(limits.max_list_items, 256);
781        assert_eq!(limits.max_json_bytes, 64 * 1024);
782        assert_eq!(limits.max_chain_depth_value, MAX_CHAIN_DEPTH_LIMIT);
783    }
784
785    #[test]
786    fn compile_max_chain_depth_zero() {
787        let expr = Expr::MaxChainDepth(0);
788        let policy = compile(&expr).unwrap();
789        assert!(matches!(policy.expr(), CompiledExpr::MaxChainDepth(0)));
790    }
791
792    #[test]
793    fn compile_max_chain_depth_at_limit() {
794        let expr = Expr::MaxChainDepth(16);
795        let policy = compile(&expr).unwrap();
796        assert!(matches!(policy.expr(), CompiledExpr::MaxChainDepth(16)));
797    }
798
799    #[test]
800    fn compile_max_chain_depth_exceeds_limit() {
801        let expr = Expr::MaxChainDepth(17);
802        let errors = compile(&expr).unwrap_err();
803        assert!(
804            errors
805                .iter()
806                .any(|e| e.message.contains("MaxChainDepth(17) exceeds limit"))
807        );
808    }
809
810    #[test]
811    fn compile_with_custom_limits() {
812        let limits = PolicyLimits {
813            max_depth: 2,
814            max_total_nodes: 10,
815            max_list_items: 5,
816            max_json_bytes: 1024,
817            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
818        };
819        let expr = Expr::And(vec![Expr::NotRevoked, Expr::NotExpired]);
820        let policy = compile_with_limits(&expr, &limits).unwrap();
821        assert!(matches!(policy.expr(), CompiledExpr::And(_)));
822    }
823
824    #[test]
825    fn compile_exceeds_max_total_nodes() {
826        let limits = PolicyLimits {
827            max_depth: 64,
828            max_total_nodes: 3,
829            max_list_items: 256,
830            max_json_bytes: 64 * 1024,
831            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
832        };
833        // 5 nodes: And + 4 children
834        let expr = Expr::And(vec![
835            Expr::NotRevoked,
836            Expr::NotExpired,
837            Expr::True,
838            Expr::False,
839        ]);
840        let errors = compile_with_limits(&expr, &limits).unwrap_err();
841        assert!(errors.iter().any(|e| e.message.contains("total nodes")));
842    }
843
844    #[test]
845    fn compile_exceeds_max_list_items() {
846        let limits = PolicyLimits {
847            max_depth: 64,
848            max_total_nodes: 1024,
849            max_list_items: 2,
850            max_json_bytes: 64 * 1024,
851            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
852        };
853        let expr = Expr::And(vec![Expr::NotRevoked, Expr::NotExpired, Expr::True]);
854        let errors = compile_with_limits(&expr, &limits).unwrap_err();
855        assert!(errors.iter().any(|e| e.message.contains("list has")));
856    }
857
858    #[test]
859    fn compile_from_json_valid() {
860        let json = r#"{"op": "True"}"#;
861        let policy = compile_from_json(json.as_bytes()).unwrap();
862        assert!(matches!(policy.expr(), CompiledExpr::True));
863    }
864
865    #[test]
866    fn compile_from_json_exceeds_max_bytes() {
867        let limits = PolicyLimits {
868            max_depth: 64,
869            max_total_nodes: 1024,
870            max_list_items: 256,
871            max_json_bytes: 10,
872            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
873        };
874        let json = r#"{"op": "true"}"#;
875        let errors = compile_from_json_with_limits(json.as_bytes(), &limits).unwrap_err();
876        assert!(errors.iter().any(|e| e.message.contains("bytes")));
877    }
878
879    #[test]
880    fn compile_from_json_invalid_json() {
881        let json = r#"{"op": "true""#; // missing closing brace
882        let errors = compile_from_json(json.as_bytes()).unwrap_err();
883        assert!(errors.iter().any(|e| e.message.contains("invalid JSON")));
884    }
885
886    #[test]
887    fn compile_issuer_in_exceeds_list_items() {
888        let limits = PolicyLimits {
889            max_depth: 64,
890            max_total_nodes: 1024,
891            max_list_items: 2,
892            max_json_bytes: 64 * 1024,
893            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
894        };
895        let expr = Expr::IssuerIn(vec![
896            "did:keri:E1".into(),
897            "did:keri:E2".into(),
898            "did:keri:E3".into(),
899        ]);
900        let errors = compile_with_limits(&expr, &limits).unwrap_err();
901        assert!(errors.iter().any(|e| e.message.contains("list has")));
902    }
903
904    #[test]
905    fn compile_role_in_exceeds_list_items() {
906        let limits = PolicyLimits {
907            max_depth: 64,
908            max_total_nodes: 1024,
909            max_list_items: 2,
910            max_json_bytes: 64 * 1024,
911            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
912        };
913        let expr = Expr::RoleIn(vec!["admin".into(), "user".into(), "guest".into()]);
914        let errors = compile_with_limits(&expr, &limits).unwrap_err();
915        assert!(errors.iter().any(|e| e.message.contains("list has")));
916    }
917
918    #[test]
919    fn compile_path_allowed_exceeds_list_items() {
920        let limits = PolicyLimits {
921            max_depth: 64,
922            max_total_nodes: 1024,
923            max_list_items: 2,
924            max_json_bytes: 64 * 1024,
925            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
926        };
927        let expr = Expr::PathAllowed(vec!["src/*".into(), "docs/*".into(), "tests/*".into()]);
928        let errors = compile_with_limits(&expr, &limits).unwrap_err();
929        assert!(errors.iter().any(|e| e.message.contains("list has")));
930    }
931
932    #[test]
933    fn compile_has_all_capabilities_exceeds_list_items() {
934        let limits = PolicyLimits {
935            max_depth: 64,
936            max_total_nodes: 1024,
937            max_list_items: 2,
938            max_json_bytes: 64 * 1024,
939            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
940        };
941        let expr = Expr::HasAllCapabilities(vec!["read".into(), "write".into(), "execute".into()]);
942        let errors = compile_with_limits(&expr, &limits).unwrap_err();
943        assert!(errors.iter().any(|e| e.message.contains("list has")));
944    }
945
946    #[test]
947    fn compile_attr_in_exceeds_list_items() {
948        let limits = PolicyLimits {
949            max_depth: 64,
950            max_total_nodes: 1024,
951            max_list_items: 2,
952            max_json_bytes: 64 * 1024,
953            max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
954        };
955        let expr = Expr::AttrIn {
956            key: "team".into(),
957            values: vec!["alpha".into(), "beta".into(), "gamma".into()],
958        };
959        let errors = compile_with_limits(&expr, &limits).unwrap_err();
960        assert!(errors.iter().any(|e| e.message.contains("list has")));
961    }
962
963    #[test]
964    fn compile_from_json_with_has_capability() {
965        let json = r#"{"op": "HasCapability", "args": "sign_commit"}"#;
966        let policy = compile_from_json(json.as_bytes()).unwrap();
967        match policy.expr() {
968            CompiledExpr::HasCapability(cap) => assert_eq!(cap.as_str(), "sign_commit"),
969            _ => panic!("expected HasCapability"),
970        }
971    }
972
973    #[test]
974    fn compile_from_json_with_and() {
975        let json = r#"{"op": "And", "args": [{"op": "NotRevoked"}, {"op": "NotExpired"}]}"#;
976        let policy = compile_from_json(json.as_bytes()).unwrap();
977        match policy.expr() {
978            CompiledExpr::And(children) => assert_eq!(children.len(), 2),
979            _ => panic!("expected And"),
980        }
981    }
982
983    #[test]
984    fn compile_flat_policy_within_bounds() {
985        // A policy with many children but within limits
986        let limits = PolicyLimits::default();
987        let children: Vec<_> = (0..100).map(|_| Expr::NotRevoked).collect();
988        let expr = Expr::And(children);
989        let policy = compile_with_limits(&expr, &limits).unwrap();
990        match policy.expr() {
991            CompiledExpr::And(children) => assert_eq!(children.len(), 100),
992            _ => panic!("expected And"),
993        }
994    }
995}