1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum VerdictKind {
47 Allow,
49 Deny,
51 RequireApproval,
53}
54
55impl VerdictKind {
56 #[inline]
58 pub fn is_deny(self) -> bool {
59 matches!(self, VerdictKind::Deny)
60 }
61
62 #[inline]
64 pub fn is_allow(self) -> bool {
65 matches!(self, VerdictKind::Allow)
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct ResolvedMatch {
92 pub matched: bool,
94 pub is_deny: bool,
96 pub is_conditional: bool,
98 pub priority: u32,
100 pub rule_override_deny: bool,
102 pub context_deny: bool,
104 pub require_approval: bool,
106 pub condition_fired: bool,
108 pub condition_verdict: VerdictKind,
110 pub on_no_match_continue: bool,
112 pub all_constraints_skipped: bool,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum VerdictOutcome {
123 Decided(VerdictKind),
125 Continue,
127}
128
129#[inline]
141#[must_use = "security verdicts must not be discarded"]
142pub fn compute_single_verdict(rm: &ResolvedMatch) -> VerdictOutcome {
143 if !rm.matched {
144 return VerdictOutcome::Continue;
145 }
146
147 if rm.rule_override_deny {
149 return VerdictOutcome::Decided(VerdictKind::Deny);
150 }
151
152 if rm.context_deny {
154 return VerdictOutcome::Decided(VerdictKind::Deny);
155 }
156
157 if rm.is_deny {
159 return VerdictOutcome::Decided(VerdictKind::Deny);
160 }
161
162 if rm.is_conditional {
163 if rm.require_approval {
165 return VerdictOutcome::Decided(VerdictKind::RequireApproval);
166 }
167
168 if rm.all_constraints_skipped {
170 if rm.on_no_match_continue {
171 return VerdictOutcome::Continue;
172 }
173 return VerdictOutcome::Decided(VerdictKind::Deny);
174 }
175
176 if rm.condition_fired {
177 return VerdictOutcome::Decided(rm.condition_verdict);
178 }
179
180 if rm.on_no_match_continue {
182 return VerdictOutcome::Continue;
183 }
184 return VerdictOutcome::Decided(VerdictKind::Allow);
185 }
186
187 VerdictOutcome::Decided(VerdictKind::Allow)
189}
190
191#[must_use = "security verdicts must not be discarded"]
208pub fn compute_verdict(resolved: &[ResolvedMatch]) -> VerdictKind {
209 for rm in resolved {
212 match compute_single_verdict(rm) {
213 VerdictOutcome::Decided(kind) => return kind,
214 VerdictOutcome::Continue => continue,
215 }
216 }
217 VerdictKind::Deny
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn allow_policy(priority: u32) -> ResolvedMatch {
226 ResolvedMatch {
227 matched: true,
228 is_deny: false,
229 is_conditional: false,
230 priority,
231 rule_override_deny: false,
232 context_deny: false,
233 require_approval: false,
234 condition_fired: false,
235 condition_verdict: VerdictKind::Deny,
236 on_no_match_continue: false,
237 all_constraints_skipped: false,
238 }
239 }
240
241 fn deny_policy(priority: u32) -> ResolvedMatch {
242 ResolvedMatch {
243 matched: true,
244 is_deny: true,
245 is_conditional: false,
246 priority,
247 rule_override_deny: false,
248 context_deny: false,
249 require_approval: false,
250 condition_fired: false,
251 condition_verdict: VerdictKind::Deny,
252 on_no_match_continue: false,
253 all_constraints_skipped: false,
254 }
255 }
256
257 fn unmatched_policy(priority: u32) -> ResolvedMatch {
258 ResolvedMatch {
259 matched: false,
260 is_deny: false,
261 is_conditional: false,
262 priority,
263 rule_override_deny: false,
264 context_deny: false,
265 require_approval: false,
266 condition_fired: false,
267 condition_verdict: VerdictKind::Deny,
268 on_no_match_continue: false,
269 all_constraints_skipped: false,
270 }
271 }
272
273 fn conditional_continue(priority: u32) -> ResolvedMatch {
274 ResolvedMatch {
275 matched: true,
276 is_deny: false,
277 is_conditional: true,
278 priority,
279 rule_override_deny: false,
280 context_deny: false,
281 require_approval: false,
282 condition_fired: false,
283 condition_verdict: VerdictKind::Deny,
284 on_no_match_continue: true,
285 all_constraints_skipped: false,
286 }
287 }
288
289 fn conditional_fired_allow(priority: u32) -> ResolvedMatch {
290 ResolvedMatch {
291 matched: true,
292 is_deny: false,
293 is_conditional: true,
294 priority,
295 rule_override_deny: false,
296 context_deny: false,
297 require_approval: false,
298 condition_fired: true,
299 condition_verdict: VerdictKind::Allow,
300 on_no_match_continue: false,
301 all_constraints_skipped: false,
302 }
303 }
304
305 fn conditional_fired_deny(priority: u32) -> ResolvedMatch {
306 ResolvedMatch {
307 matched: true,
308 is_deny: false,
309 is_conditional: true,
310 priority,
311 rule_override_deny: false,
312 context_deny: false,
313 require_approval: false,
314 condition_fired: true,
315 condition_verdict: VerdictKind::Deny,
316 on_no_match_continue: false,
317 all_constraints_skipped: false,
318 }
319 }
320
321 #[test]
324 fn test_v1_empty_produces_deny() {
325 assert_eq!(compute_verdict(&[]), VerdictKind::Deny);
326 }
327
328 #[test]
331 fn test_v2_all_unmatched_produces_deny() {
332 let resolved = vec![unmatched_policy(100), unmatched_policy(50)];
333 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
334 }
335
336 #[test]
339 fn test_v3_allow_from_allow_policy() {
340 let resolved = vec![allow_policy(100)];
341 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
342 }
343
344 #[test]
345 fn test_v3_allow_not_from_deny_policy() {
346 let resolved = vec![deny_policy(100)];
347 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
348 }
349
350 #[test]
351 fn test_v3_allow_not_from_rule_override() {
352 let mut rm = allow_policy(100);
353 rm.rule_override_deny = true;
354 assert_eq!(compute_verdict(&[rm]), VerdictKind::Deny);
355 }
356
357 #[test]
358 fn test_v3_allow_not_from_context_deny() {
359 let mut rm = allow_policy(100);
360 rm.context_deny = true;
361 assert_eq!(compute_verdict(&[rm]), VerdictKind::Deny);
362 }
363
364 #[test]
367 fn test_v4_rule_override_forces_deny() {
368 let mut rm = allow_policy(100);
369 rm.rule_override_deny = true;
370 assert_eq!(
371 compute_single_verdict(&rm),
372 VerdictOutcome::Decided(VerdictKind::Deny)
373 );
374 }
375
376 #[test]
377 fn test_v4_rule_override_on_deny_policy() {
378 let mut rm = deny_policy(100);
379 rm.rule_override_deny = true;
380 assert_eq!(
381 compute_single_verdict(&rm),
382 VerdictOutcome::Decided(VerdictKind::Deny)
383 );
384 }
385
386 #[test]
387 fn test_v4_rule_override_on_conditional() {
388 let mut rm = conditional_fired_allow(100);
389 rm.rule_override_deny = true;
390 assert_eq!(
391 compute_single_verdict(&rm),
392 VerdictOutcome::Decided(VerdictKind::Deny)
393 );
394 }
395
396 #[test]
402 fn test_v6_higher_priority_deny_wins() {
403 let resolved = vec![deny_policy(100), allow_policy(50)];
405 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
406 }
407
408 #[test]
409 fn test_v6_higher_priority_allow_wins() {
410 let resolved = vec![allow_policy(100), deny_policy(50)];
412 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
413 }
414
415 #[test]
418 fn test_v7_deny_before_allow_at_equal_priority() {
419 let resolved = vec![deny_policy(100), allow_policy(100)];
421 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
422 }
423
424 #[test]
427 fn test_v8_conditional_continue_skips_to_next() {
428 assert_eq!(
429 compute_single_verdict(&conditional_continue(100)),
430 VerdictOutcome::Continue,
431 );
432 }
433
434 #[test]
435 fn test_v8_conditional_continue_then_allow() {
436 let resolved = vec![conditional_continue(100), allow_policy(50)];
437 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
438 }
439
440 #[test]
441 fn test_v8_conditional_continue_then_deny() {
442 let resolved = vec![conditional_continue(100), deny_policy(50)];
443 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
444 }
445
446 #[test]
447 fn test_v8_all_conditional_continue_produces_deny() {
448 let resolved = vec![conditional_continue(100), conditional_continue(50)];
449 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
450 }
451
452 #[test]
455 fn test_conditional_fired_allow() {
456 let resolved = vec![conditional_fired_allow(100)];
457 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
458 }
459
460 #[test]
461 fn test_conditional_fired_deny() {
462 let resolved = vec![conditional_fired_deny(100)];
463 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
464 }
465
466 #[test]
469 fn test_conditional_no_fire_no_continue_implicit_allow() {
470 let mut rm = conditional_continue(100);
471 rm.on_no_match_continue = false;
472 assert_eq!(
473 compute_single_verdict(&rm),
474 VerdictOutcome::Decided(VerdictKind::Allow),
475 );
476 }
477
478 #[test]
481 fn test_require_approval_verdict() {
482 let mut rm = conditional_continue(100);
483 rm.require_approval = true;
484 assert_eq!(
485 compute_single_verdict(&rm),
486 VerdictOutcome::Decided(VerdictKind::RequireApproval),
487 );
488 }
489
490 #[test]
493 fn test_all_constraints_skipped_continue() {
494 let mut rm = conditional_continue(100);
495 rm.all_constraints_skipped = true;
496 assert_eq!(compute_single_verdict(&rm), VerdictOutcome::Continue,);
497 }
498
499 #[test]
500 fn test_all_constraints_skipped_no_continue_deny() {
501 let mut rm = conditional_continue(100);
502 rm.all_constraints_skipped = true;
503 rm.on_no_match_continue = false;
504 assert_eq!(
505 compute_single_verdict(&rm),
506 VerdictOutcome::Decided(VerdictKind::Deny),
507 );
508 }
509
510 #[test]
513 fn test_context_deny_overrides_allow() {
514 let mut rm = allow_policy(100);
515 rm.context_deny = true;
516 assert_eq!(compute_verdict(&[rm]), VerdictKind::Deny);
517 }
518
519 #[test]
520 fn test_context_deny_on_conditional_overrides_fired_allow() {
521 let mut rm = conditional_fired_allow(100);
522 rm.context_deny = true;
523 assert_eq!(compute_verdict(&[rm]), VerdictKind::Deny);
524 }
525
526 #[test]
529 fn test_mixed_unmatched_then_allow() {
530 let resolved = vec![unmatched_policy(200), allow_policy(100)];
531 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
532 }
533
534 #[test]
535 fn test_mixed_continue_then_continue_then_deny() {
536 let resolved = vec![
537 conditional_continue(100),
538 conditional_continue(90),
539 deny_policy(80),
540 ];
541 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
542 }
543
544 #[test]
545 fn test_conditional_fired_require_approval() {
546 let mut rm = conditional_continue(100);
547 rm.condition_fired = true;
548 rm.condition_verdict = VerdictKind::RequireApproval;
549 assert_eq!(
550 compute_single_verdict(&rm),
551 VerdictOutcome::Decided(VerdictKind::RequireApproval),
552 );
553 }
554
555 #[test]
556 fn test_rule_override_before_context_deny() {
557 let mut rm = allow_policy(100);
558 rm.rule_override_deny = true;
559 rm.context_deny = true;
560 assert_eq!(
562 compute_single_verdict(&rm),
563 VerdictOutcome::Decided(VerdictKind::Deny),
564 );
565 }
566
567 #[test]
570 fn test_many_unmatched_then_conditional_fired_allow() {
571 let resolved = vec![
572 unmatched_policy(200),
573 unmatched_policy(150),
574 unmatched_policy(100),
575 conditional_fired_allow(50),
576 ];
577 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
578 }
579
580 #[test]
581 fn test_conditional_chain_with_fired_deny_at_end() {
582 let resolved = vec![
583 conditional_continue(100),
584 conditional_continue(90),
585 conditional_continue(80),
586 conditional_fired_deny(70),
587 ];
588 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
589 }
590
591 #[test]
592 fn test_rule_override_on_first_match_skips_later_allow() {
593 let mut overridden = allow_policy(100);
594 overridden.rule_override_deny = true;
595 let resolved = vec![overridden, allow_policy(50)];
596 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
597 }
598
599 #[test]
600 fn test_context_deny_on_first_match_skips_later_allow() {
601 let mut ctx_deny = allow_policy(100);
602 ctx_deny.context_deny = true;
603 let resolved = vec![ctx_deny, allow_policy(50)];
604 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
605 }
606
607 #[test]
608 fn test_require_approval_in_conditional_chain() {
609 let mut approval = conditional_continue(90);
610 approval.require_approval = true;
611 let resolved = vec![conditional_continue(100), approval];
612 assert_eq!(compute_verdict(&resolved), VerdictKind::RequireApproval);
613 }
614
615 #[test]
616 fn test_mixed_unmatched_continue_deny() {
617 let resolved = vec![
618 unmatched_policy(200),
619 conditional_continue(150),
620 unmatched_policy(100),
621 deny_policy(50),
622 ];
623 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
624 }
625
626 #[test]
627 fn test_all_constraints_skipped_in_chain_then_allow() {
628 let mut skipped = conditional_continue(100);
629 skipped.all_constraints_skipped = true;
630 let resolved = vec![skipped, allow_policy(50)];
631 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
632 }
633
634 #[test]
635 fn test_all_constraints_skipped_no_continue_in_chain() {
636 let mut skipped = conditional_continue(100);
637 skipped.all_constraints_skipped = true;
638 skipped.on_no_match_continue = false;
639 let resolved = vec![skipped, allow_policy(50)];
640 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
642 }
643
644 #[test]
645 fn test_single_unmatched_produces_deny() {
646 let resolved = vec![unmatched_policy(100)];
647 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
648 }
649
650 #[test]
651 fn test_conditional_implicit_allow_when_no_continue() {
652 let mut rm = conditional_continue(100);
653 rm.on_no_match_continue = false;
654 let resolved = vec![rm];
656 assert_eq!(compute_verdict(&resolved), VerdictKind::Allow);
657 }
658
659 #[test]
660 fn test_large_policy_set_first_match_deny() {
661 let mut resolved: Vec<ResolvedMatch> = (0..50).map(|i| unmatched_policy(200 - i)).collect();
662 resolved.push(deny_policy(100));
663 for i in 0..20 {
665 resolved.push(unmatched_policy(50 - i));
666 }
667 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
668 }
669
670 #[test]
671 fn test_large_policy_set_all_unmatched() {
672 let resolved: Vec<ResolvedMatch> = (0..100).map(|i| unmatched_policy(200 - i)).collect();
673 assert_eq!(compute_verdict(&resolved), VerdictKind::Deny);
674 }
675
676 #[test]
677 fn test_conditional_fired_deny_verdict_from_constraint() {
678 let mut rm = conditional_continue(100);
679 rm.condition_fired = true;
680 rm.condition_verdict = VerdictKind::Deny;
681 assert_eq!(
682 compute_single_verdict(&rm),
683 VerdictOutcome::Decided(VerdictKind::Deny),
684 );
685 }
686}