1use crate::compiled::{CompiledExpr, CompiledPolicy};
7use crate::expr::Expr;
8use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob};
9
10const MAX_ATTR_KEY_LEN: usize = 64;
12
13pub const MAX_CHAIN_DEPTH_LIMIT: u32 = 16;
15
16#[non_exhaustive]
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PolicyLimits {
24 pub max_depth: u32,
26 pub max_total_nodes: u32,
28 pub max_list_items: usize,
30 pub max_json_bytes: usize,
32 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, max_chain_depth_value: MAX_CHAIN_DEPTH_LIMIT,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct CompileError {
51 pub path: String,
53 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
65pub fn compile(expr: &Expr) -> Result<CompiledPolicy, Vec<CompileError>> {
81 compile_with_limits(expr, &PolicyLimits::default())
82}
83
84pub fn compile_from_json(json: &[u8]) -> Result<CompiledPolicy, Vec<CompileError>> {
94 compile_from_json_with_limits(json, &PolicyLimits::default())
95}
96
97pub 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
127pub 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 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 *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 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 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 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 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 #[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 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""#; 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 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}