1use std::collections::BTreeMap;
44use std::fmt;
45use std::future::Future;
46use std::net::IpAddr;
47use std::pin::Pin;
48use std::sync::Arc;
49
50use async_trait::async_trait;
51use tracing::warn;
52
53use super::client_ip::ProxyPolicy;
54use super::expr::Condition;
55use super::{ConnectionContext, IdentifierContext};
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Verdict {
60 Pass,
62 Fail(String),
64 Undecided(String),
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Stage {
73 Connection,
75 Identifiers,
77}
78
79impl Stage {
80 #[must_use]
82 pub fn as_str(self) -> &'static str {
83 match self {
84 Self::Connection => "connection",
85 Self::Identifiers => "identifiers",
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct StageSet {
93 pub connection: bool,
94 pub identifiers: bool,
95}
96
97impl StageSet {
98 #[must_use]
100 pub const fn both() -> Self {
101 Self {
102 connection: true,
103 identifiers: true,
104 }
105 }
106
107 #[must_use]
108 pub const fn connection_only() -> Self {
109 Self {
110 connection: true,
111 identifiers: false,
112 }
113 }
114
115 #[must_use]
116 pub const fn identifiers_only() -> Self {
117 Self {
118 connection: false,
119 identifiers: true,
120 }
121 }
122
123 #[must_use]
125 pub const fn none() -> Self {
126 Self {
127 connection: false,
128 identifiers: false,
129 }
130 }
131
132 #[must_use]
133 pub const fn contains(self, stage: Stage) -> bool {
134 match stage {
135 Stage::Connection => self.connection,
136 Stage::Identifiers => self.identifiers,
137 }
138 }
139
140 #[must_use]
141 pub const fn intersect(self, other: Self) -> Self {
142 Self {
143 connection: self.connection && other.connection,
144 identifiers: self.identifiers && other.identifiers,
145 }
146 }
147
148 #[must_use]
149 pub const fn is_empty(self) -> bool {
150 !self.connection && !self.identifiers
151 }
152}
153
154impl fmt::Display for StageSet {
155 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match (self.connection, self.identifiers) {
157 (true, true) => formatter.write_str("connection and identifiers"),
158 (true, false) => formatter.write_str("connection only"),
159 (false, true) => formatter.write_str("identifiers only"),
160 (false, false) => formatter.write_str("no stage"),
161 }
162 }
163}
164
165#[async_trait]
172pub trait Check: Send + Sync {
173 fn kind(&self) -> &'static str;
176
177 fn stages(&self) -> StageSet;
179
180 async fn check_connection(&self, _context: &ConnectionContext<'_>) -> Verdict {
181 Verdict::Pass
182 }
183
184 async fn check_identifiers(&self, _context: &IdentifierContext<'_>) -> Verdict {
185 Verdict::Pass
186 }
187}
188
189#[must_use]
192pub fn kleene_and(left: &Verdict, right: &Verdict) -> Verdict {
193 match (left, right) {
194 (Verdict::Fail(detail), _) | (_, Verdict::Fail(detail)) => Verdict::Fail(detail.clone()),
195 (Verdict::Undecided(detail), _) | (_, Verdict::Undecided(detail)) => {
196 Verdict::Undecided(detail.clone())
197 }
198 (Verdict::Pass, Verdict::Pass) => Verdict::Pass,
199 }
200}
201
202#[must_use]
206pub fn kleene_or(left: &Verdict, right: &Verdict) -> Verdict {
207 match (left, right) {
208 (Verdict::Pass, _) | (_, Verdict::Pass) => Verdict::Pass,
209 (Verdict::Undecided(detail), _) | (_, Verdict::Undecided(detail)) => {
210 Verdict::Undecided(detail.clone())
211 }
212 (Verdict::Fail(detail), Verdict::Fail(_)) => Verdict::Fail(detail.clone()),
213 }
214}
215
216#[must_use]
218pub fn kleene_not(verdict: &Verdict) -> Verdict {
219 match verdict {
220 Verdict::Pass => Verdict::Fail("the condition was negated".to_string()),
221 Verdict::Fail(_) => Verdict::Pass,
222 Verdict::Undecided(detail) => Verdict::Undecided(detail.clone()),
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub enum Effect {
229 Allow,
230 Deny,
231}
232
233impl Effect {
234 #[must_use]
235 pub fn as_str(self) -> &'static str {
236 match self {
237 Self::Allow => "allow",
238 Self::Deny => "deny",
239 }
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum Mode {
246 Enforce,
247 Warn,
251}
252
253#[derive(Debug, Clone)]
255pub struct Rule {
256 pub name: String,
257 pub when: Condition,
258 pub then: Effect,
259 pub message: Option<String>,
261 pub mode: Mode,
262}
263
264#[derive(Debug, Clone)]
266struct CompiledRule {
267 rule: Rule,
268 stages: StageSet,
269}
270
271struct CheckSlot {
272 kind: &'static str,
273 stages: StageSet,
274 check: Arc<dyn Check>,
275}
276
277#[derive(Debug, Clone)]
279pub struct CheckOutcome {
280 pub name: String,
281 pub kind: &'static str,
282 pub verdict: Verdict,
283}
284
285#[derive(Debug, Clone)]
287pub struct WarnedRule {
288 pub name: String,
289 pub then: Effect,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum Outcome {
295 Allow,
296 Deny(String),
298 Undecided(String),
301}
302
303#[derive(Debug, Clone)]
305pub struct Evaluation {
306 pub outcome: Outcome,
307 pub matched: Option<String>,
310 pub checks: Vec<CheckOutcome>,
313 pub warned: Vec<WarnedRule>,
314}
315
316#[derive(Debug, Clone, Copy)]
318pub struct CheckSummary<'a> {
319 pub name: &'a str,
320 pub kind: &'static str,
321 pub stages: StageSet,
322}
323
324#[derive(Debug, Clone, Copy)]
326pub struct RuleSummary<'a> {
327 pub name: &'a str,
328 pub when: &'a Condition,
329 pub then: Effect,
330 pub mode: Mode,
331 pub stages: StageSet,
332}
333
334struct PendingUnknown {
336 rule: String,
337 then: Effect,
338 detail: String,
339}
340
341pub struct FilterPolicy {
346 checks: BTreeMap<String, CheckSlot>,
347 rules: Vec<CompiledRule>,
348 default_effect: Effect,
349 proxy: ProxyPolicy,
350}
351
352impl fmt::Debug for FilterPolicy {
353 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
356 formatter
357 .debug_struct("FilterPolicy")
358 .field(
359 "checks",
360 &self
361 .checks
362 .iter()
363 .map(|(name, slot)| format!("{name}: {}", slot.kind))
364 .collect::<Vec<_>>(),
365 )
366 .field(
367 "rules",
368 &self
369 .rules
370 .iter()
371 .map(|compiled| {
372 format!(
373 "{}: {} -> {}",
374 compiled.rule.name,
375 compiled.rule.when,
376 compiled.rule.then.as_str()
377 )
378 })
379 .collect::<Vec<_>>(),
380 )
381 .field("default", &self.default_effect.as_str())
382 .field("proxy", &self.proxy)
383 .finish()
384 }
385}
386
387impl Default for FilterPolicy {
388 fn default() -> Self {
392 Self {
393 checks: BTreeMap::new(),
394 rules: Vec::new(),
395 default_effect: Effect::Deny,
396 proxy: ProxyPolicy::default(),
397 }
398 }
399}
400
401impl FilterPolicy {
402 #[must_use]
410 pub fn new(
411 checks: Vec<(String, Arc<dyn Check>)>,
412 rules: Vec<Rule>,
413 default_effect: Effect,
414 proxy: ProxyPolicy,
415 ) -> Self {
416 let checks: BTreeMap<String, CheckSlot> = checks
417 .into_iter()
418 .map(|(name, check)| {
419 let slot = CheckSlot {
420 kind: check.kind(),
421 stages: check.stages(),
422 check,
423 };
424 (name, slot)
425 })
426 .collect();
427
428 let rules = rules
429 .into_iter()
430 .map(|rule| {
431 let stages = stages_for(&rule.when, &checks);
432 CompiledRule { rule, stages }
433 })
434 .collect();
435
436 Self {
437 checks,
438 rules,
439 default_effect,
440 proxy,
441 }
442 }
443
444 pub fn proxy(&self) -> &ProxyPolicy {
446 &self.proxy
447 }
448
449 #[must_use]
451 pub fn default_effect(&self) -> Effect {
452 self.default_effect
453 }
454
455 #[must_use]
460 pub fn has_rules_at(&self, stage: Stage) -> bool {
461 self.rules
462 .iter()
463 .any(|compiled| compiled.stages.contains(stage))
464 }
465
466 #[must_use]
468 pub fn is_active(&self) -> bool {
469 !self.rules.is_empty()
470 }
471
472 pub fn checks(&self) -> Vec<CheckSummary<'_>> {
475 self.checks
476 .iter()
477 .map(|(name, slot)| CheckSummary {
478 name,
479 kind: slot.kind,
480 stages: slot.stages,
481 })
482 .collect()
483 }
484
485 pub fn rules(&self) -> Vec<RuleSummary<'_>> {
487 self.rules
488 .iter()
489 .map(|compiled| RuleSummary {
490 name: &compiled.rule.name,
491 when: &compiled.rule.when,
492 then: compiled.rule.then,
493 mode: compiled.rule.mode,
494 stages: compiled.stages,
495 })
496 .collect()
497 }
498
499 #[must_use]
504 pub fn needs_eab(&self) -> bool {
505 self.checks.values().any(|slot| slot.kind == "eab")
506 }
507
508 pub async fn evaluate_connection(&self, context: &ConnectionContext<'_>) -> Evaluation {
513 self.evaluate(Hook::Connection(context)).await
514 }
515
516 pub async fn evaluate_identifiers(&self, context: &IdentifierContext<'_>) -> Evaluation {
518 self.evaluate(Hook::Identifiers(context)).await
519 }
520
521 pub async fn check_connection(&self, context: &ConnectionContext<'_>) -> Outcome {
523 let evaluation = self.evaluate_connection(context).await;
524 log_decision(&evaluation, Stage::Connection.as_str(), context.client_ip);
525 evaluation.outcome
526 }
527
528 pub async fn check_identifiers(&self, context: &IdentifierContext<'_>) -> Outcome {
534 let evaluation = self.evaluate_identifiers(context).await;
535 log_decision(&evaluation, context.stage.as_str(), context.client_ip);
536 evaluation.outcome
537 }
538
539 async fn evaluate(&self, hook: Hook<'_>) -> Evaluation {
556 let stage = hook.stage();
557 let applicable: Vec<&CompiledRule> = self
558 .rules
559 .iter()
560 .filter(|compiled| compiled.stages.contains(stage))
561 .collect();
562
563 if applicable.is_empty() {
565 return Evaluation {
566 outcome: Outcome::Allow,
567 matched: None,
568 checks: Vec::new(),
569 warned: Vec::new(),
570 };
571 }
572
573 let mut run = Run {
574 policy: self,
575 hook,
576 stage,
577 memo: BTreeMap::new(),
578 trace: Vec::new(),
579 };
580 let mut decision: Option<&Rule> = None;
581 let mut warned = Vec::new();
582 let mut pending: Vec<PendingUnknown> = Vec::new();
583 let mut last_rule_start = 0;
587
588 for compiled in applicable {
589 last_rule_start = run.trace.len();
590 match run.eval(&compiled.rule.when).await {
591 Verdict::Pass => {
592 if compiled.rule.mode == Mode::Warn {
593 warn!(
594 event = "filter_rule_warned",
595 outcome = "advisory",
596 rule = %compiled.rule.name,
597 then = compiled.rule.then.as_str(),
598 stage = stage.as_str(),
599 "rule matched in warn mode and did not decide",
600 );
601 warned.push(WarnedRule {
602 name: compiled.rule.name.clone(),
603 then: compiled.rule.then,
604 });
605 continue;
606 }
607 decision = Some(&compiled.rule);
608 break;
609 }
610 Verdict::Fail(_) => continue,
611 Verdict::Undecided(detail) => {
612 if compiled.rule.mode == Mode::Enforce {
613 pending.push(PendingUnknown {
614 rule: compiled.rule.name.clone(),
615 then: compiled.rule.then,
616 detail,
617 });
618 }
619 }
620 }
621 }
622
623 let effect = decision.map_or(self.default_effect, |rule| rule.then);
624
625 if let Some(unknown) = pending.into_iter().find(|entry| entry.then != effect) {
626 return Evaluation {
627 outcome: Outcome::Undecided(format!(
628 "rule `{}` could not be evaluated ({}), and it would have \
629 decided differently from the rule that did",
630 unknown.rule, unknown.detail
631 )),
632 matched: decision.map(|rule| rule.name.clone()),
633 checks: run.trace,
634 warned,
635 };
636 }
637
638 let outcome = match effect {
639 Effect::Allow => Outcome::Allow,
640 Effect::Deny => Outcome::Deny(denial_detail(decision, &run.trace, last_rule_start)),
641 };
642
643 Evaluation {
644 outcome,
645 matched: decision.map(|rule| rule.name.clone()),
646 checks: run.trace,
647 warned,
648 }
649 }
650}
651
652fn log_decision(evaluation: &Evaluation, hook: &str, client_ip: Option<IpAddr>) {
663 let rule = evaluation.matched.as_deref().unwrap_or("default");
664 let source = evaluation
665 .checks
666 .iter()
667 .find(|outcome| matches!(outcome.verdict, Verdict::Fail(_) | Verdict::Undecided(_)));
668 let check = source.map(|outcome| outcome.name.as_str());
669 let kind = source.map(|outcome| outcome.kind);
670
671 match &evaluation.outcome {
672 Outcome::Allow => {}
673 Outcome::Deny(detail) => tracing::warn!(
674 event = "filter_denied",
675 outcome = "failure",
676 check = ?check,
677 filter = ?kind,
678 rule,
679 hook,
680 client_ip = ?client_ip,
681 detail = %detail,
682 ),
683 Outcome::Undecided(detail) => tracing::error!(
684 event = "filter_failed",
685 outcome = "failure",
686 check = ?check,
687 filter = ?kind,
688 rule,
689 hook,
690 client_ip = ?client_ip,
691 detail = %detail,
692 ),
693 }
694}
695
696fn stages_for(condition: &Condition, checks: &BTreeMap<String, CheckSlot>) -> StageSet {
698 condition
699 .check_names()
700 .into_iter()
701 .fold(StageSet::both(), |accumulated, name| {
702 let stages = checks
703 .get(name)
704 .map_or_else(StageSet::none, |slot| slot.stages);
705 accumulated.intersect(stages)
706 })
707}
708
709fn denial_detail(
730 decision: Option<&Rule>,
731 trace: &[CheckOutcome],
732 last_rule_start: usize,
733) -> String {
734 if let Some(rule) = decision {
735 return rule
736 .message
737 .clone()
738 .unwrap_or_else(|| format!("refused by policy rule `{}`", rule.name));
739 }
740
741 let first_failure = |slice: &[CheckOutcome]| {
742 slice.iter().find_map(|outcome| match &outcome.verdict {
743 Verdict::Fail(detail) => Some(detail.clone()),
744 _ => None,
745 })
746 };
747
748 first_failure(trace.get(last_rule_start..).unwrap_or_default())
749 .or_else(|| first_failure(trace))
750 .unwrap_or_else(|| "no policy rule permits this request".to_string())
751}
752
753#[derive(Clone, Copy)]
755enum Hook<'a> {
756 Connection(&'a ConnectionContext<'a>),
757 Identifiers(&'a IdentifierContext<'a>),
758}
759
760impl Hook<'_> {
761 fn stage(self) -> Stage {
762 match self {
763 Self::Connection(_) => Stage::Connection,
764 Self::Identifiers(_) => Stage::Identifiers,
765 }
766 }
767}
768
769struct Run<'a> {
771 policy: &'a FilterPolicy,
772 hook: Hook<'a>,
773 stage: Stage,
774 memo: BTreeMap<String, Verdict>,
778 trace: Vec<CheckOutcome>,
779}
780
781type VerdictFuture<'a> = Pin<Box<dyn Future<Output = Verdict> + Send + 'a>>;
782
783impl Run<'_> {
784 fn eval<'s>(&'s mut self, condition: &'s Condition) -> VerdictFuture<'s> {
794 Box::pin(async move {
795 match condition {
796 Condition::Check(name) => self.eval_check(name).await,
797 Condition::Not(inner) => kleene_not(&self.eval(inner).await),
798 Condition::And(left, right) => {
799 let left = self.eval(left).await;
800 if matches!(left, Verdict::Fail(_)) {
801 return left;
802 }
803 let right = self.eval(right).await;
804 kleene_and(&left, &right)
805 }
806 Condition::Or(left, right) => {
807 let left = self.eval(left).await;
808 if matches!(left, Verdict::Pass) {
809 return left;
810 }
811 let right = self.eval(right).await;
812 kleene_or(&left, &right)
813 }
814 }
815 })
816 }
817
818 async fn eval_check(&mut self, name: &str) -> Verdict {
819 if let Some(cached) = self.memo.get(name) {
820 return cached.clone();
821 }
822
823 let Some(slot) = self.policy.checks.get(name) else {
824 return Verdict::Undecided(format!("check `{name}` is not configured"));
828 };
829
830 if !slot.stages.contains(self.stage) {
835 return Verdict::Undecided(format!(
836 "check `{name}` cannot decide at the {} stage",
837 self.stage.as_str()
838 ));
839 }
840
841 let kind = slot.kind;
842 let check = Arc::clone(&slot.check);
843
844 let verdict = match self.hook {
845 Hook::Connection(context) => check.check_connection(context).await,
846 Hook::Identifiers(context) => check.check_identifiers(context).await,
847 };
848
849 self.memo.insert(name.to_string(), verdict.clone());
850 self.trace.push(CheckOutcome {
851 name: name.to_string(),
852 kind,
853 verdict: verdict.clone(),
854 });
855 verdict
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use std::sync::atomic::{AtomicUsize, Ordering};
862
863 use axum::http::Method;
864
865 use super::*;
866
867 struct StubCheck {
873 verdict: Verdict,
874 stages: StageSet,
875 calls: Arc<AtomicUsize>,
876 }
877
878 impl StubCheck {
879 fn with(verdict: Verdict, stages: StageSet) -> (Arc<dyn Check>, Arc<AtomicUsize>) {
880 let calls = Arc::new(AtomicUsize::new(0));
881 let check = Arc::new(Self {
882 verdict,
883 stages,
884 calls: Arc::clone(&calls),
885 });
886 (check, calls)
887 }
888
889 fn passing() -> (Arc<dyn Check>, Arc<AtomicUsize>) {
890 Self::with(Verdict::Pass, StageSet::both())
891 }
892
893 fn failing() -> (Arc<dyn Check>, Arc<AtomicUsize>) {
894 Self::with(Verdict::Fail("stub refused".to_string()), StageSet::both())
895 }
896
897 fn undecided() -> (Arc<dyn Check>, Arc<AtomicUsize>) {
898 Self::with(
899 Verdict::Undecided("stub is down".to_string()),
900 StageSet::both(),
901 )
902 }
903 }
904
905 #[async_trait]
906 impl Check for StubCheck {
907 fn kind(&self) -> &'static str {
908 "stub"
909 }
910
911 fn stages(&self) -> StageSet {
912 self.stages
913 }
914
915 async fn check_connection(&self, _context: &ConnectionContext<'_>) -> Verdict {
916 self.calls.fetch_add(1, Ordering::SeqCst);
917 self.verdict.clone()
918 }
919
920 async fn check_identifiers(&self, _context: &IdentifierContext<'_>) -> Verdict {
921 self.calls.fetch_add(1, Ordering::SeqCst);
922 self.verdict.clone()
923 }
924 }
925
926 fn rule(name: &str, when: &str, then: Effect) -> Rule {
927 Rule {
928 name: name.to_string(),
929 when: Condition::parse(when).expect("test condition should parse"),
930 then,
931 message: None,
932 mode: Mode::Enforce,
933 }
934 }
935
936 fn connection_context() -> ConnectionContext<'static> {
937 ConnectionContext {
938 client_ip: Some("10.0.0.5".parse().expect("literal address")),
939 method: &Method::POST,
940 path: "/newOrder",
941 }
942 }
943
944 async fn decide(policy: &FilterPolicy) -> Outcome {
945 policy
946 .evaluate_connection(&connection_context())
947 .await
948 .outcome
949 }
950
951 #[test]
956 fn kleene_and_is_complete() {
957 let pass = Verdict::Pass;
958 let fail = Verdict::Fail("no".to_string());
959 let unknown = Verdict::Undecided("down".to_string());
960
961 let cases = [
962 (&pass, &pass, &pass),
963 (&pass, &fail, &fail),
964 (&pass, &unknown, &unknown),
965 (&fail, &pass, &fail),
966 (&fail, &fail, &fail),
967 (&fail, &unknown, &fail),
969 (&unknown, &pass, &unknown),
970 (&unknown, &fail, &fail),
971 (&unknown, &unknown, &unknown),
972 ];
973
974 for (left, right, expected) in cases {
975 assert!(
976 same_kind(&kleene_and(left, right), expected),
977 "{left:?} and {right:?} should be {expected:?}"
978 );
979 }
980 }
981
982 #[test]
983 fn kleene_or_is_complete() {
984 let pass = Verdict::Pass;
985 let fail = Verdict::Fail("no".to_string());
986 let unknown = Verdict::Undecided("down".to_string());
987
988 let cases = [
989 (&pass, &pass, &pass),
990 (&pass, &fail, &pass),
991 (&pass, &unknown, &pass),
994 (&fail, &pass, &pass),
995 (&fail, &fail, &fail),
996 (&fail, &unknown, &unknown),
997 (&unknown, &pass, &pass),
998 (&unknown, &fail, &unknown),
999 (&unknown, &unknown, &unknown),
1000 ];
1001
1002 for (left, right, expected) in cases {
1003 assert!(
1004 same_kind(&kleene_or(left, right), expected),
1005 "{left:?} or {right:?} should be {expected:?}"
1006 );
1007 }
1008 }
1009
1010 #[test]
1011 fn kleene_not_leaves_the_unknown_alone() {
1012 assert!(same_kind(
1013 &kleene_not(&Verdict::Pass),
1014 &Verdict::Fail(String::new())
1015 ));
1016 assert!(same_kind(
1017 &kleene_not(&Verdict::Fail("no".to_string())),
1018 &Verdict::Pass
1019 ));
1020 assert!(same_kind(
1021 &kleene_not(&Verdict::Undecided("down".to_string())),
1022 &Verdict::Undecided(String::new())
1023 ));
1024 }
1025
1026 fn same_kind(left: &Verdict, right: &Verdict) -> bool {
1028 matches!(
1029 (left, right),
1030 (Verdict::Pass, Verdict::Pass)
1031 | (Verdict::Fail(_), Verdict::Fail(_))
1032 | (Verdict::Undecided(_), Verdict::Undecided(_))
1033 )
1034 }
1035
1036 #[tokio::test]
1039 async fn a_failing_left_operand_skips_the_right_of_an_and() {
1040 let (left, left_calls) = StubCheck::failing();
1041 let (right, right_calls) = StubCheck::passing();
1042 let policy = FilterPolicy::new(
1043 vec![("left".to_string(), left), ("right".to_string(), right)],
1044 vec![rule("r", "left and right", Effect::Allow)],
1045 Effect::Deny,
1046 ProxyPolicy::default(),
1047 );
1048
1049 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1050 assert_eq!(left_calls.load(Ordering::SeqCst), 1);
1051 assert_eq!(right_calls.load(Ordering::SeqCst), 0, "right was evaluated");
1052 }
1053
1054 #[tokio::test]
1055 async fn a_passing_left_operand_skips_the_right_of_an_or() {
1056 let (left, left_calls) = StubCheck::passing();
1057 let (right, right_calls) = StubCheck::failing();
1058 let policy = FilterPolicy::new(
1059 vec![("left".to_string(), left), ("right".to_string(), right)],
1060 vec![rule("r", "left or right", Effect::Allow)],
1061 Effect::Deny,
1062 ProxyPolicy::default(),
1063 );
1064
1065 assert_eq!(decide(&policy).await, Outcome::Allow);
1066 assert_eq!(left_calls.load(Ordering::SeqCst), 1);
1067 assert_eq!(right_calls.load(Ordering::SeqCst), 0, "right was evaluated");
1068 }
1069
1070 #[tokio::test]
1074 async fn an_undecided_left_operand_still_evaluates_the_right() {
1075 let (left, left_calls) = StubCheck::undecided();
1076 let (right, right_calls) = StubCheck::passing();
1077 let policy = FilterPolicy::new(
1078 vec![("left".to_string(), left), ("right".to_string(), right)],
1079 vec![rule("r", "left or right", Effect::Allow)],
1080 Effect::Deny,
1081 ProxyPolicy::default(),
1082 );
1083
1084 assert_eq!(decide(&policy).await, Outcome::Allow);
1085 assert_eq!(left_calls.load(Ordering::SeqCst), 1);
1086 assert_eq!(right_calls.load(Ordering::SeqCst), 1);
1087 }
1088
1089 #[tokio::test]
1090 async fn a_check_named_twice_runs_once() {
1091 let (check, calls) = StubCheck::passing();
1092 let policy = FilterPolicy::new(
1093 vec![("only".to_string(), check)],
1094 vec![rule("r", "only and (only or only)", Effect::Allow)],
1095 Effect::Deny,
1096 ProxyPolicy::default(),
1097 );
1098
1099 assert_eq!(decide(&policy).await, Outcome::Allow);
1100 assert_eq!(calls.load(Ordering::SeqCst), 1);
1101 }
1102
1103 #[tokio::test]
1104 async fn memoisation_spans_rules_within_one_stage() {
1105 let (check, calls) = StubCheck::failing();
1106 let policy = FilterPolicy::new(
1107 vec![("only".to_string(), check)],
1108 vec![
1109 rule("first", "only", Effect::Allow),
1110 rule("second", "only", Effect::Allow),
1111 ],
1112 Effect::Deny,
1113 ProxyPolicy::default(),
1114 );
1115
1116 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1117 assert_eq!(calls.load(Ordering::SeqCst), 1);
1118 }
1119
1120 #[tokio::test]
1123 async fn the_first_matching_rule_decides_and_later_rules_never_run() {
1124 let (first, _) = StubCheck::passing();
1125 let (second, second_calls) = StubCheck::passing();
1126 let policy = FilterPolicy::new(
1127 vec![("first".to_string(), first), ("second".to_string(), second)],
1128 vec![
1129 rule("allow-it", "first", Effect::Allow),
1130 rule("deny-it", "second", Effect::Deny),
1131 ],
1132 Effect::Deny,
1133 ProxyPolicy::default(),
1134 );
1135
1136 assert_eq!(decide(&policy).await, Outcome::Allow);
1137 assert_eq!(second_calls.load(Ordering::SeqCst), 0);
1138 }
1139
1140 #[tokio::test]
1141 async fn a_stage_with_no_applicable_rules_allows() {
1142 let (check, calls) = StubCheck::with(
1143 Verdict::Fail("no".to_string()),
1144 StageSet::identifiers_only(),
1145 );
1146 let policy = FilterPolicy::new(
1147 vec![("names".to_string(), check)],
1148 vec![rule("names-only", "names", Effect::Allow)],
1149 Effect::Deny,
1150 ProxyPolicy::default(),
1151 );
1152
1153 assert_eq!(decide(&policy).await, Outcome::Allow);
1157 assert_eq!(calls.load(Ordering::SeqCst), 0);
1158 assert!(!policy.has_rules_at(Stage::Connection));
1159 assert!(policy.has_rules_at(Stage::Identifiers));
1160 }
1161
1162 #[tokio::test]
1163 async fn the_default_applies_only_once_a_rule_was_applicable() {
1164 let (check, _) = StubCheck::failing();
1165 let policy = FilterPolicy::new(
1166 vec![("no".to_string(), check)],
1167 vec![rule("never", "no", Effect::Allow)],
1168 Effect::Allow,
1169 ProxyPolicy::default(),
1170 );
1171
1172 assert_eq!(decide(&policy).await, Outcome::Allow);
1173 }
1174
1175 #[tokio::test]
1176 async fn a_warn_rule_matches_without_deciding() {
1177 let (check, _) = StubCheck::passing();
1178 let mut warned = rule("would-deny", "yes", Effect::Deny);
1179 warned.mode = Mode::Warn;
1180
1181 let policy = FilterPolicy::new(
1182 vec![("yes".to_string(), check)],
1183 vec![warned],
1184 Effect::Allow,
1185 ProxyPolicy::default(),
1186 );
1187
1188 let evaluation = policy.evaluate_connection(&connection_context()).await;
1189 assert_eq!(evaluation.outcome, Outcome::Allow);
1190 assert_eq!(evaluation.matched, None);
1191 assert_eq!(evaluation.warned.len(), 1);
1192 assert_eq!(evaluation.warned[0].name, "would-deny");
1193 assert_eq!(evaluation.warned[0].then, Effect::Deny);
1194 }
1195
1196 #[tokio::test]
1197 async fn the_enforcing_twin_of_a_warn_rule_denies() {
1198 let (check, _) = StubCheck::passing();
1199 let policy = FilterPolicy::new(
1200 vec![("yes".to_string(), check)],
1201 vec![rule("deny-it", "yes", Effect::Deny)],
1202 Effect::Allow,
1203 ProxyPolicy::default(),
1204 );
1205
1206 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1207 }
1208
1209 #[tokio::test]
1212 async fn an_unknown_rule_poisons_a_differing_answer() {
1213 let (down, _) = StubCheck::undecided();
1214 let policy = FilterPolicy::new(
1215 vec![("inventory".to_string(), down)],
1216 vec![rule("inventory-owned", "inventory", Effect::Allow)],
1217 Effect::Deny,
1218 ProxyPolicy::default(),
1219 );
1220
1221 assert!(matches!(decide(&policy).await, Outcome::Undecided(_)));
1224 }
1225
1226 #[tokio::test]
1229 async fn an_unknown_rule_is_harmless_when_the_answer_agrees() {
1230 let (down, _) = StubCheck::undecided();
1231 let (up, _) = StubCheck::passing();
1232 let policy = FilterPolicy::new(
1233 vec![("inventory".to_string(), down), ("mgmt".to_string(), up)],
1234 vec![
1235 rule("inventory-owned", "inventory", Effect::Allow),
1236 rule("mgmt-bypass", "mgmt", Effect::Allow),
1237 ],
1238 Effect::Deny,
1239 ProxyPolicy::default(),
1240 );
1241
1242 assert_eq!(decide(&policy).await, Outcome::Allow);
1243 }
1244
1245 #[tokio::test]
1248 async fn an_or_absorbs_the_outage_within_a_single_rule() {
1249 let (down, _) = StubCheck::undecided();
1250 let (up, _) = StubCheck::passing();
1251 let policy = FilterPolicy::new(
1252 vec![("inventory".to_string(), down), ("mgmt".to_string(), up)],
1253 vec![rule("reachable", "mgmt or inventory", Effect::Allow)],
1254 Effect::Deny,
1255 ProxyPolicy::default(),
1256 );
1257
1258 assert_eq!(decide(&policy).await, Outcome::Allow);
1259 }
1260
1261 #[tokio::test]
1264 async fn the_inventory_alone_is_still_an_outage() {
1265 let (down, _) = StubCheck::undecided();
1266 let policy = FilterPolicy::new(
1267 vec![("inventory".to_string(), down)],
1268 vec![rule("reachable", "inventory", Effect::Allow)],
1269 Effect::Deny,
1270 ProxyPolicy::default(),
1271 );
1272
1273 assert!(matches!(decide(&policy).await, Outcome::Undecided(_)));
1274 }
1275
1276 #[tokio::test]
1277 async fn a_warn_rule_that_cannot_be_evaluated_poisons_nothing() {
1278 let (down, _) = StubCheck::undecided();
1279 let mut dry_run = rule("inventory-owned", "inventory", Effect::Allow);
1280 dry_run.mode = Mode::Warn;
1281
1282 let policy = FilterPolicy::new(
1283 vec![("inventory".to_string(), down)],
1284 vec![dry_run],
1285 Effect::Deny,
1286 ProxyPolicy::default(),
1287 );
1288
1289 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1290 }
1291
1292 #[tokio::test]
1295 async fn a_rule_message_is_shown_verbatim() {
1296 let (check, _) = StubCheck::passing();
1297 let mut denying = rule("no-tenants", "yes", Effect::Deny);
1298 denying.message = Some("this address owns no such name".to_string());
1299
1300 let policy = FilterPolicy::new(
1301 vec![("yes".to_string(), check)],
1302 vec![denying],
1303 Effect::Allow,
1304 ProxyPolicy::default(),
1305 );
1306
1307 assert_eq!(
1308 decide(&policy).await,
1309 Outcome::Deny("this address owns no such name".to_string())
1310 );
1311 }
1312
1313 #[tokio::test]
1314 async fn a_denying_rule_without_a_message_names_itself() {
1315 let (check, _) = StubCheck::passing();
1316 let policy = FilterPolicy::new(
1317 vec![("yes".to_string(), check)],
1318 vec![rule("no-tenants", "yes", Effect::Deny)],
1319 Effect::Allow,
1320 ProxyPolicy::default(),
1321 );
1322
1323 assert_eq!(
1324 decide(&policy).await,
1325 Outcome::Deny("refused by policy rule `no-tenants`".to_string())
1326 );
1327 }
1328
1329 #[tokio::test]
1330 async fn falling_through_to_the_default_reports_the_first_refusing_check() {
1331 let (check, _) = StubCheck::failing();
1332 let policy = FilterPolicy::new(
1333 vec![("addr".to_string(), check)],
1334 vec![rule("permitted", "addr", Effect::Allow)],
1335 Effect::Deny,
1336 ProxyPolicy::default(),
1337 );
1338
1339 assert_eq!(
1340 decide(&policy).await,
1341 Outcome::Deny("stub refused".to_string())
1342 );
1343 }
1344
1345 #[tokio::test]
1349 async fn a_default_deny_quotes_the_last_rule_not_the_first_bypass() {
1350 let (bypass, _) = StubCheck::with(
1351 Verdict::Fail("path /newOrder is not allowed".to_string()),
1352 StageSet::both(),
1353 );
1354 let (main, _) = StubCheck::with(
1355 Verdict::Fail("address 203.0.113.9 is not allowed".to_string()),
1356 StageSet::both(),
1357 );
1358
1359 let policy = FilterPolicy::new(
1360 vec![
1361 ("public-paths".to_string(), bypass),
1362 ("mgmt-net".to_string(), main),
1363 ],
1364 vec![
1365 rule("public", "public-paths", Effect::Allow),
1366 rule("mgmt-bypass", "mgmt-net", Effect::Allow),
1367 ],
1368 Effect::Deny,
1369 ProxyPolicy::default(),
1370 );
1371
1372 assert_eq!(
1373 decide(&policy).await,
1374 Outcome::Deny("address 203.0.113.9 is not allowed".to_string())
1375 );
1376 }
1377
1378 #[tokio::test]
1381 async fn a_default_deny_widens_when_the_last_rule_refused_nothing() {
1382 let (failing, _) = StubCheck::failing();
1383 let (passing, _) = StubCheck::passing();
1384
1385 let policy = FilterPolicy::new(
1386 vec![
1387 ("first".to_string(), failing),
1388 ("second".to_string(), passing),
1389 ],
1390 vec![
1391 rule("early", "first", Effect::Allow),
1392 rule("late", "not second", Effect::Allow),
1395 ],
1396 Effect::Deny,
1397 ProxyPolicy::default(),
1398 );
1399
1400 assert_eq!(
1401 decide(&policy).await,
1402 Outcome::Deny("stub refused".to_string())
1403 );
1404 }
1405
1406 #[tokio::test]
1407 async fn a_default_deny_with_nothing_to_report_says_so() {
1408 let (check, _) = StubCheck::passing();
1409 let policy = FilterPolicy::new(
1414 vec![("yes".to_string(), check)],
1415 vec![rule("permitted", "not yes", Effect::Allow)],
1416 Effect::Deny,
1417 ProxyPolicy::default(),
1418 );
1419
1420 let evaluation = policy.evaluate_connection(&connection_context()).await;
1421 assert_eq!(
1422 evaluation.outcome,
1423 Outcome::Deny("no policy rule permits this request".to_string())
1424 );
1425 assert_eq!(evaluation.checks.len(), 1);
1426 assert_eq!(evaluation.checks[0].verdict, Verdict::Pass);
1427 }
1428
1429 #[tokio::test]
1432 async fn the_trace_records_every_evaluated_check_in_order() {
1433 let (first, _) = StubCheck::passing();
1434 let (second, _) = StubCheck::failing();
1435 let policy = FilterPolicy::new(
1436 vec![("first".to_string(), first), ("second".to_string(), second)],
1437 vec![rule("r", "first and second", Effect::Allow)],
1438 Effect::Deny,
1439 ProxyPolicy::default(),
1440 );
1441
1442 let evaluation = policy.evaluate_connection(&connection_context()).await;
1443 let names: Vec<&str> = evaluation
1444 .checks
1445 .iter()
1446 .map(|outcome| outcome.name.as_str())
1447 .collect();
1448 assert_eq!(names, vec!["first", "second"]);
1449 assert_eq!(evaluation.checks[0].kind, "stub");
1450 }
1451
1452 #[test]
1455 fn a_rule_takes_the_intersection_of_its_checks_stages() {
1456 let (anywhere, _) = StubCheck::with(Verdict::Pass, StageSet::both());
1457 let (names_only, _) = StubCheck::with(Verdict::Pass, StageSet::identifiers_only());
1458 let policy = FilterPolicy::new(
1459 vec![
1460 ("anywhere".to_string(), anywhere),
1461 ("names".to_string(), names_only),
1462 ],
1463 vec![rule("mixed", "anywhere or names", Effect::Allow)],
1464 Effect::Deny,
1465 ProxyPolicy::default(),
1466 );
1467
1468 assert!(!policy.has_rules_at(Stage::Connection));
1469 assert!(policy.has_rules_at(Stage::Identifiers));
1470 }
1471
1472 #[test]
1473 fn a_rule_naming_an_unknown_check_is_never_applicable() {
1474 let policy = FilterPolicy::new(
1475 Vec::new(),
1476 vec![rule("broken", "nonexistent", Effect::Allow)],
1477 Effect::Deny,
1478 ProxyPolicy::default(),
1479 );
1480
1481 assert!(!policy.has_rules_at(Stage::Connection));
1482 assert!(!policy.has_rules_at(Stage::Identifiers));
1483 assert!(policy.is_active());
1484 }
1485
1486 #[test]
1487 fn stage_sets_describe_themselves() {
1488 assert_eq!(StageSet::both().to_string(), "connection and identifiers");
1489 assert_eq!(StageSet::connection_only().to_string(), "connection only");
1490 assert_eq!(StageSet::identifiers_only().to_string(), "identifiers only");
1491 assert_eq!(StageSet::none().to_string(), "no stage");
1492 assert!(StageSet::none().is_empty());
1493 assert!(
1494 StageSet::connection_only()
1495 .intersect(StageSet::identifiers_only())
1496 .is_empty()
1497 );
1498 assert_eq!(Stage::Connection.as_str(), "connection");
1499 assert_eq!(Stage::Identifiers.as_str(), "identifiers");
1500 assert_eq!(Effect::Allow.as_str(), "allow");
1501 assert_eq!(Effect::Deny.as_str(), "deny");
1502 }
1503
1504 #[test]
1505 fn the_default_policy_decides_nothing() {
1506 let policy = FilterPolicy::default();
1507 assert!(!policy.is_active());
1508 assert!(!policy.has_rules_at(Stage::Connection));
1509 assert_eq!(policy.default_effect(), Effect::Deny);
1510 assert!(format!("{policy:?}").contains("FilterPolicy"));
1511 }
1512
1513 #[test]
1514 fn the_debug_rendering_names_checks_and_rules() {
1515 let (check, _) = StubCheck::passing();
1516 let policy = FilterPolicy::new(
1517 vec![("mgmt".to_string(), check)],
1518 vec![rule("bypass", "mgmt", Effect::Allow)],
1519 Effect::Deny,
1520 ProxyPolicy::default(),
1521 );
1522
1523 let rendered = format!("{policy:?}");
1524 assert!(rendered.contains("mgmt: stub"), "{rendered}");
1525 assert!(rendered.contains("bypass: mgmt -> allow"), "{rendered}");
1526 }
1527}