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
253impl Mode {
254 #[must_use]
261 pub fn as_str(self) -> &'static str {
262 match self {
263 Self::Enforce => "enforce",
264 Self::Warn => "warn",
265 }
266 }
267}
268
269#[derive(Debug, Clone)]
271pub struct Rule {
272 pub name: String,
273 pub when: Condition,
274 pub then: Effect,
275 pub message: Option<String>,
277 pub mode: Mode,
278}
279
280#[derive(Debug, Clone)]
282struct CompiledRule {
283 rule: Rule,
284 stages: StageSet,
285}
286
287struct CheckSlot {
288 kind: &'static str,
289 stages: StageSet,
290 check: Arc<dyn Check>,
291}
292
293#[derive(Debug, Clone)]
295pub struct CheckOutcome {
296 pub name: String,
297 pub kind: &'static str,
298 pub verdict: Verdict,
299}
300
301#[derive(Debug, Clone)]
303pub struct WarnedRule {
304 pub name: String,
305 pub then: Effect,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum Outcome {
311 Allow,
312 Deny(String),
314 Undecided(String),
317}
318
319#[derive(Debug, Clone)]
321pub struct Evaluation {
322 pub outcome: Outcome,
323 pub matched: Option<String>,
326 pub checks: Vec<CheckOutcome>,
329 pub warned: Vec<WarnedRule>,
330}
331
332#[derive(Debug, Clone, Copy)]
334pub struct CheckSummary<'a> {
335 pub name: &'a str,
336 pub kind: &'static str,
337 pub stages: StageSet,
338}
339
340#[derive(Debug, Clone, Copy)]
342pub struct RuleSummary<'a> {
343 pub name: &'a str,
344 pub when: &'a Condition,
345 pub then: Effect,
346 pub mode: Mode,
347 pub stages: StageSet,
348}
349
350struct PendingUnknown {
352 rule: String,
353 then: Effect,
354 detail: String,
355}
356
357pub struct FilterPolicy {
362 checks: BTreeMap<String, CheckSlot>,
363 rules: Vec<CompiledRule>,
364 default_effect: Effect,
365 proxy: ProxyPolicy,
366}
367
368impl fmt::Debug for FilterPolicy {
369 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
372 formatter
373 .debug_struct("FilterPolicy")
374 .field(
375 "checks",
376 &self
377 .checks
378 .iter()
379 .map(|(name, slot)| format!("{name}: {}", slot.kind))
380 .collect::<Vec<_>>(),
381 )
382 .field(
383 "rules",
384 &self
385 .rules
386 .iter()
387 .map(|compiled| {
388 format!(
389 "{}: {} -> {}",
390 compiled.rule.name,
391 compiled.rule.when,
392 compiled.rule.then.as_str()
393 )
394 })
395 .collect::<Vec<_>>(),
396 )
397 .field("default", &self.default_effect.as_str())
398 .field("proxy", &self.proxy)
399 .finish()
400 }
401}
402
403impl Default for FilterPolicy {
404 fn default() -> Self {
408 Self {
409 checks: BTreeMap::new(),
410 rules: Vec::new(),
411 default_effect: Effect::Deny,
412 proxy: ProxyPolicy::default(),
413 }
414 }
415}
416
417impl FilterPolicy {
418 #[must_use]
426 pub fn new(
427 checks: Vec<(String, Arc<dyn Check>)>,
428 rules: Vec<Rule>,
429 default_effect: Effect,
430 proxy: ProxyPolicy,
431 ) -> Self {
432 let checks: BTreeMap<String, CheckSlot> = checks
433 .into_iter()
434 .map(|(name, check)| {
435 let slot = CheckSlot {
436 kind: check.kind(),
437 stages: check.stages(),
438 check,
439 };
440 (name, slot)
441 })
442 .collect();
443
444 let rules = rules
445 .into_iter()
446 .map(|rule| {
447 let stages = stages_for(&rule.when, &checks);
448 CompiledRule { rule, stages }
449 })
450 .collect();
451
452 Self {
453 checks,
454 rules,
455 default_effect,
456 proxy,
457 }
458 }
459
460 pub fn proxy(&self) -> &ProxyPolicy {
462 &self.proxy
463 }
464
465 #[must_use]
467 pub fn default_effect(&self) -> Effect {
468 self.default_effect
469 }
470
471 #[must_use]
476 pub fn has_rules_at(&self, stage: Stage) -> bool {
477 self.rules
478 .iter()
479 .any(|compiled| compiled.stages.contains(stage))
480 }
481
482 #[must_use]
484 pub fn is_active(&self) -> bool {
485 !self.rules.is_empty()
486 }
487
488 pub fn checks(&self) -> Vec<CheckSummary<'_>> {
491 self.checks
492 .iter()
493 .map(|(name, slot)| CheckSummary {
494 name,
495 kind: slot.kind,
496 stages: slot.stages,
497 })
498 .collect()
499 }
500
501 pub fn rules(&self) -> Vec<RuleSummary<'_>> {
503 self.rules
504 .iter()
505 .map(|compiled| RuleSummary {
506 name: &compiled.rule.name,
507 when: &compiled.rule.when,
508 then: compiled.rule.then,
509 mode: compiled.rule.mode,
510 stages: compiled.stages,
511 })
512 .collect()
513 }
514
515 #[must_use]
520 pub fn needs_eab(&self) -> bool {
521 self.checks.values().any(|slot| slot.kind == "eab")
522 }
523
524 pub async fn evaluate_connection(&self, context: &ConnectionContext<'_>) -> Evaluation {
529 self.evaluate(Hook::Connection(context)).await
530 }
531
532 pub async fn evaluate_identifiers(&self, context: &IdentifierContext<'_>) -> Evaluation {
534 self.evaluate(Hook::Identifiers(context)).await
535 }
536
537 pub async fn check_connection(&self, context: &ConnectionContext<'_>) -> Outcome {
539 let evaluation = self.evaluate_connection(context).await;
540 log_decision(&evaluation, Stage::Connection.as_str(), context.client_ip);
541 evaluation.outcome
542 }
543
544 pub async fn check_identifiers(&self, context: &IdentifierContext<'_>) -> Outcome {
550 let evaluation = self.evaluate_identifiers(context).await;
551 log_decision(&evaluation, context.stage.as_str(), context.client_ip);
552 evaluation.outcome
553 }
554
555 async fn evaluate(&self, hook: Hook<'_>) -> Evaluation {
572 let stage = hook.stage();
573 let applicable: Vec<&CompiledRule> = self
574 .rules
575 .iter()
576 .filter(|compiled| compiled.stages.contains(stage))
577 .collect();
578
579 if applicable.is_empty() {
581 return Evaluation {
582 outcome: Outcome::Allow,
583 matched: None,
584 checks: Vec::new(),
585 warned: Vec::new(),
586 };
587 }
588
589 let mut run = Run {
590 policy: self,
591 hook,
592 stage,
593 memo: BTreeMap::new(),
594 trace: Vec::new(),
595 };
596 let mut decision: Option<&Rule> = None;
597 let mut warned = Vec::new();
598 let mut pending: Vec<PendingUnknown> = Vec::new();
599 let mut last_rule_start = 0;
603
604 for compiled in applicable {
605 last_rule_start = run.trace.len();
606 match run.eval(&compiled.rule.when).await {
607 Verdict::Pass => {
608 if compiled.rule.mode == Mode::Warn {
609 warn!(
610 event = "filter_rule_warned",
611 outcome = "advisory",
612 rule = %compiled.rule.name,
613 then = compiled.rule.then.as_str(),
614 stage = stage.as_str(),
615 "rule matched in warn mode and did not decide",
616 );
617 warned.push(WarnedRule {
618 name: compiled.rule.name.clone(),
619 then: compiled.rule.then,
620 });
621 continue;
622 }
623 decision = Some(&compiled.rule);
624 break;
625 }
626 Verdict::Fail(_) => continue,
627 Verdict::Undecided(detail) => {
628 if compiled.rule.mode == Mode::Enforce {
629 pending.push(PendingUnknown {
630 rule: compiled.rule.name.clone(),
631 then: compiled.rule.then,
632 detail,
633 });
634 }
635 }
636 }
637 }
638
639 let effect = decision.map_or(self.default_effect, |rule| rule.then);
640
641 if let Some(unknown) = pending.into_iter().find(|entry| entry.then != effect) {
642 return Evaluation {
643 outcome: Outcome::Undecided(format!(
644 "rule `{}` could not be evaluated ({}), and it would have \
645 decided differently from the rule that did",
646 unknown.rule, unknown.detail
647 )),
648 matched: decision.map(|rule| rule.name.clone()),
649 checks: run.trace,
650 warned,
651 };
652 }
653
654 let outcome = match effect {
655 Effect::Allow => Outcome::Allow,
656 Effect::Deny => Outcome::Deny(denial_detail(decision, &run.trace, last_rule_start)),
657 };
658
659 Evaluation {
660 outcome,
661 matched: decision.map(|rule| rule.name.clone()),
662 checks: run.trace,
663 warned,
664 }
665 }
666}
667
668fn log_decision(evaluation: &Evaluation, hook: &str, client_ip: Option<IpAddr>) {
679 let rule = evaluation.matched.as_deref().unwrap_or("default");
680 let source = evaluation
681 .checks
682 .iter()
683 .find(|outcome| matches!(outcome.verdict, Verdict::Fail(_) | Verdict::Undecided(_)));
684 let check = source.map(|outcome| outcome.name.as_str());
685 let kind = source.map(|outcome| outcome.kind);
686
687 match &evaluation.outcome {
688 Outcome::Allow => {}
689 Outcome::Deny(detail) => tracing::warn!(
690 event = "filter_denied",
691 outcome = "failure",
692 check = ?check,
693 filter = ?kind,
694 rule,
695 hook,
696 client_ip = ?client_ip,
697 detail = %detail,
698 ),
699 Outcome::Undecided(detail) => tracing::error!(
700 event = "filter_failed",
701 outcome = "failure",
702 check = ?check,
703 filter = ?kind,
704 rule,
705 hook,
706 client_ip = ?client_ip,
707 detail = %detail,
708 ),
709 }
710}
711
712fn stages_for(condition: &Condition, checks: &BTreeMap<String, CheckSlot>) -> StageSet {
714 condition
715 .check_names()
716 .into_iter()
717 .fold(StageSet::both(), |accumulated, name| {
718 let stages = checks
719 .get(name)
720 .map_or_else(StageSet::none, |slot| slot.stages);
721 accumulated.intersect(stages)
722 })
723}
724
725fn denial_detail(
746 decision: Option<&Rule>,
747 trace: &[CheckOutcome],
748 last_rule_start: usize,
749) -> String {
750 if let Some(rule) = decision {
751 return rule
752 .message
753 .clone()
754 .unwrap_or_else(|| format!("refused by policy rule `{}`", rule.name));
755 }
756
757 let first_failure = |slice: &[CheckOutcome]| {
758 slice.iter().find_map(|outcome| match &outcome.verdict {
759 Verdict::Fail(detail) => Some(detail.clone()),
760 _ => None,
761 })
762 };
763
764 first_failure(trace.get(last_rule_start..).unwrap_or_default())
765 .or_else(|| first_failure(trace))
766 .unwrap_or_else(|| "no policy rule permits this request".to_string())
767}
768
769#[derive(Clone, Copy)]
771enum Hook<'a> {
772 Connection(&'a ConnectionContext<'a>),
773 Identifiers(&'a IdentifierContext<'a>),
774}
775
776impl Hook<'_> {
777 fn stage(self) -> Stage {
778 match self {
779 Self::Connection(_) => Stage::Connection,
780 Self::Identifiers(_) => Stage::Identifiers,
781 }
782 }
783}
784
785struct Run<'a> {
787 policy: &'a FilterPolicy,
788 hook: Hook<'a>,
789 stage: Stage,
790 memo: BTreeMap<String, Verdict>,
794 trace: Vec<CheckOutcome>,
795}
796
797type VerdictFuture<'a> = Pin<Box<dyn Future<Output = Verdict> + Send + 'a>>;
798
799impl Run<'_> {
800 fn eval<'s>(&'s mut self, condition: &'s Condition) -> VerdictFuture<'s> {
810 Box::pin(async move {
811 match condition {
812 Condition::Check(name) => self.eval_check(name).await,
813 Condition::Not(inner) => kleene_not(&self.eval(inner).await),
814 Condition::And(left, right) => {
815 let left = self.eval(left).await;
816 if matches!(left, Verdict::Fail(_)) {
817 return left;
818 }
819 let right = self.eval(right).await;
820 kleene_and(&left, &right)
821 }
822 Condition::Or(left, right) => {
823 let left = self.eval(left).await;
824 if matches!(left, Verdict::Pass) {
825 return left;
826 }
827 let right = self.eval(right).await;
828 kleene_or(&left, &right)
829 }
830 }
831 })
832 }
833
834 async fn eval_check(&mut self, name: &str) -> Verdict {
835 if let Some(cached) = self.memo.get(name) {
836 return cached.clone();
837 }
838
839 let Some(slot) = self.policy.checks.get(name) else {
840 return Verdict::Undecided(format!("check `{name}` is not configured"));
844 };
845
846 if !slot.stages.contains(self.stage) {
851 return Verdict::Undecided(format!(
852 "check `{name}` cannot decide at the {} stage",
853 self.stage.as_str()
854 ));
855 }
856
857 let kind = slot.kind;
858 let check = Arc::clone(&slot.check);
859
860 let verdict = match self.hook {
861 Hook::Connection(context) => check.check_connection(context).await,
862 Hook::Identifiers(context) => check.check_identifiers(context).await,
863 };
864
865 self.memo.insert(name.to_string(), verdict.clone());
866 self.trace.push(CheckOutcome {
867 name: name.to_string(),
868 kind,
869 verdict: verdict.clone(),
870 });
871 verdict
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use std::sync::atomic::{AtomicUsize, Ordering};
878
879 use axum::http::Method;
880
881 use super::*;
882
883 struct StubCheck {
889 verdict: Verdict,
890 stages: StageSet,
891 calls: Arc<AtomicUsize>,
892 }
893
894 impl StubCheck {
895 fn with(verdict: Verdict, stages: StageSet) -> (Arc<dyn Check>, Arc<AtomicUsize>) {
896 let calls = Arc::new(AtomicUsize::new(0));
897 let check = Arc::new(Self {
898 verdict,
899 stages,
900 calls: Arc::clone(&calls),
901 });
902 (check, calls)
903 }
904
905 fn passing() -> (Arc<dyn Check>, Arc<AtomicUsize>) {
906 Self::with(Verdict::Pass, StageSet::both())
907 }
908
909 fn failing() -> (Arc<dyn Check>, Arc<AtomicUsize>) {
910 Self::with(Verdict::Fail("stub refused".to_string()), StageSet::both())
911 }
912
913 fn undecided() -> (Arc<dyn Check>, Arc<AtomicUsize>) {
914 Self::with(
915 Verdict::Undecided("stub is down".to_string()),
916 StageSet::both(),
917 )
918 }
919 }
920
921 #[async_trait]
922 impl Check for StubCheck {
923 fn kind(&self) -> &'static str {
924 "stub"
925 }
926
927 fn stages(&self) -> StageSet {
928 self.stages
929 }
930
931 async fn check_connection(&self, _context: &ConnectionContext<'_>) -> Verdict {
932 self.calls.fetch_add(1, Ordering::SeqCst);
933 self.verdict.clone()
934 }
935
936 async fn check_identifiers(&self, _context: &IdentifierContext<'_>) -> Verdict {
937 self.calls.fetch_add(1, Ordering::SeqCst);
938 self.verdict.clone()
939 }
940 }
941
942 fn rule(name: &str, when: &str, then: Effect) -> Rule {
943 Rule {
944 name: name.to_string(),
945 when: Condition::parse(when).expect("test condition should parse"),
946 then,
947 message: None,
948 mode: Mode::Enforce,
949 }
950 }
951
952 fn connection_context() -> ConnectionContext<'static> {
953 ConnectionContext {
954 client_ip: Some("10.0.0.5".parse().expect("literal address")),
955 method: &Method::POST,
956 path: "/newOrder",
957 }
958 }
959
960 async fn decide(policy: &FilterPolicy) -> Outcome {
961 policy
962 .evaluate_connection(&connection_context())
963 .await
964 .outcome
965 }
966
967 #[test]
972 fn kleene_and_is_complete() {
973 let pass = Verdict::Pass;
974 let fail = Verdict::Fail("no".to_string());
975 let unknown = Verdict::Undecided("down".to_string());
976
977 let cases = [
978 (&pass, &pass, &pass),
979 (&pass, &fail, &fail),
980 (&pass, &unknown, &unknown),
981 (&fail, &pass, &fail),
982 (&fail, &fail, &fail),
983 (&fail, &unknown, &fail),
985 (&unknown, &pass, &unknown),
986 (&unknown, &fail, &fail),
987 (&unknown, &unknown, &unknown),
988 ];
989
990 for (left, right, expected) in cases {
991 assert!(
992 same_kind(&kleene_and(left, right), expected),
993 "{left:?} and {right:?} should be {expected:?}"
994 );
995 }
996 }
997
998 #[test]
999 fn kleene_or_is_complete() {
1000 let pass = Verdict::Pass;
1001 let fail = Verdict::Fail("no".to_string());
1002 let unknown = Verdict::Undecided("down".to_string());
1003
1004 let cases = [
1005 (&pass, &pass, &pass),
1006 (&pass, &fail, &pass),
1007 (&pass, &unknown, &pass),
1010 (&fail, &pass, &pass),
1011 (&fail, &fail, &fail),
1012 (&fail, &unknown, &unknown),
1013 (&unknown, &pass, &pass),
1014 (&unknown, &fail, &unknown),
1015 (&unknown, &unknown, &unknown),
1016 ];
1017
1018 for (left, right, expected) in cases {
1019 assert!(
1020 same_kind(&kleene_or(left, right), expected),
1021 "{left:?} or {right:?} should be {expected:?}"
1022 );
1023 }
1024 }
1025
1026 #[test]
1027 fn kleene_not_leaves_the_unknown_alone() {
1028 assert!(same_kind(
1029 &kleene_not(&Verdict::Pass),
1030 &Verdict::Fail(String::new())
1031 ));
1032 assert!(same_kind(
1033 &kleene_not(&Verdict::Fail("no".to_string())),
1034 &Verdict::Pass
1035 ));
1036 assert!(same_kind(
1037 &kleene_not(&Verdict::Undecided("down".to_string())),
1038 &Verdict::Undecided(String::new())
1039 ));
1040 }
1041
1042 fn same_kind(left: &Verdict, right: &Verdict) -> bool {
1044 matches!(
1045 (left, right),
1046 (Verdict::Pass, Verdict::Pass)
1047 | (Verdict::Fail(_), Verdict::Fail(_))
1048 | (Verdict::Undecided(_), Verdict::Undecided(_))
1049 )
1050 }
1051
1052 #[tokio::test]
1055 async fn a_failing_left_operand_skips_the_right_of_an_and() {
1056 let (left, left_calls) = StubCheck::failing();
1057 let (right, right_calls) = StubCheck::passing();
1058 let policy = FilterPolicy::new(
1059 vec![("left".to_string(), left), ("right".to_string(), right)],
1060 vec![rule("r", "left and right", Effect::Allow)],
1061 Effect::Deny,
1062 ProxyPolicy::default(),
1063 );
1064
1065 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
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]
1071 async fn a_passing_left_operand_skips_the_right_of_an_or() {
1072 let (left, left_calls) = StubCheck::passing();
1073 let (right, right_calls) = StubCheck::failing();
1074 let policy = FilterPolicy::new(
1075 vec![("left".to_string(), left), ("right".to_string(), right)],
1076 vec![rule("r", "left or right", Effect::Allow)],
1077 Effect::Deny,
1078 ProxyPolicy::default(),
1079 );
1080
1081 assert_eq!(decide(&policy).await, Outcome::Allow);
1082 assert_eq!(left_calls.load(Ordering::SeqCst), 1);
1083 assert_eq!(right_calls.load(Ordering::SeqCst), 0, "right was evaluated");
1084 }
1085
1086 #[tokio::test]
1090 async fn an_undecided_left_operand_still_evaluates_the_right() {
1091 let (left, left_calls) = StubCheck::undecided();
1092 let (right, right_calls) = StubCheck::passing();
1093 let policy = FilterPolicy::new(
1094 vec![("left".to_string(), left), ("right".to_string(), right)],
1095 vec![rule("r", "left or right", Effect::Allow)],
1096 Effect::Deny,
1097 ProxyPolicy::default(),
1098 );
1099
1100 assert_eq!(decide(&policy).await, Outcome::Allow);
1101 assert_eq!(left_calls.load(Ordering::SeqCst), 1);
1102 assert_eq!(right_calls.load(Ordering::SeqCst), 1);
1103 }
1104
1105 #[tokio::test]
1106 async fn a_check_named_twice_runs_once() {
1107 let (check, calls) = StubCheck::passing();
1108 let policy = FilterPolicy::new(
1109 vec![("only".to_string(), check)],
1110 vec![rule("r", "only and (only or only)", Effect::Allow)],
1111 Effect::Deny,
1112 ProxyPolicy::default(),
1113 );
1114
1115 assert_eq!(decide(&policy).await, Outcome::Allow);
1116 assert_eq!(calls.load(Ordering::SeqCst), 1);
1117 }
1118
1119 #[tokio::test]
1120 async fn memoisation_spans_rules_within_one_stage() {
1121 let (check, calls) = StubCheck::failing();
1122 let policy = FilterPolicy::new(
1123 vec![("only".to_string(), check)],
1124 vec![
1125 rule("first", "only", Effect::Allow),
1126 rule("second", "only", Effect::Allow),
1127 ],
1128 Effect::Deny,
1129 ProxyPolicy::default(),
1130 );
1131
1132 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1133 assert_eq!(calls.load(Ordering::SeqCst), 1);
1134 }
1135
1136 #[tokio::test]
1139 async fn the_first_matching_rule_decides_and_later_rules_never_run() {
1140 let (first, _) = StubCheck::passing();
1141 let (second, second_calls) = StubCheck::passing();
1142 let policy = FilterPolicy::new(
1143 vec![("first".to_string(), first), ("second".to_string(), second)],
1144 vec![
1145 rule("allow-it", "first", Effect::Allow),
1146 rule("deny-it", "second", Effect::Deny),
1147 ],
1148 Effect::Deny,
1149 ProxyPolicy::default(),
1150 );
1151
1152 assert_eq!(decide(&policy).await, Outcome::Allow);
1153 assert_eq!(second_calls.load(Ordering::SeqCst), 0);
1154 }
1155
1156 #[tokio::test]
1157 async fn a_stage_with_no_applicable_rules_allows() {
1158 let (check, calls) = StubCheck::with(
1159 Verdict::Fail("no".to_string()),
1160 StageSet::identifiers_only(),
1161 );
1162 let policy = FilterPolicy::new(
1163 vec![("names".to_string(), check)],
1164 vec![rule("names-only", "names", Effect::Allow)],
1165 Effect::Deny,
1166 ProxyPolicy::default(),
1167 );
1168
1169 assert_eq!(decide(&policy).await, Outcome::Allow);
1173 assert_eq!(calls.load(Ordering::SeqCst), 0);
1174 assert!(!policy.has_rules_at(Stage::Connection));
1175 assert!(policy.has_rules_at(Stage::Identifiers));
1176 }
1177
1178 #[tokio::test]
1179 async fn the_default_applies_only_once_a_rule_was_applicable() {
1180 let (check, _) = StubCheck::failing();
1181 let policy = FilterPolicy::new(
1182 vec![("no".to_string(), check)],
1183 vec![rule("never", "no", Effect::Allow)],
1184 Effect::Allow,
1185 ProxyPolicy::default(),
1186 );
1187
1188 assert_eq!(decide(&policy).await, Outcome::Allow);
1189 }
1190
1191 #[tokio::test]
1192 async fn a_warn_rule_matches_without_deciding() {
1193 let (check, _) = StubCheck::passing();
1194 let mut warned = rule("would-deny", "yes", Effect::Deny);
1195 warned.mode = Mode::Warn;
1196
1197 let policy = FilterPolicy::new(
1198 vec![("yes".to_string(), check)],
1199 vec![warned],
1200 Effect::Allow,
1201 ProxyPolicy::default(),
1202 );
1203
1204 let evaluation = policy.evaluate_connection(&connection_context()).await;
1205 assert_eq!(evaluation.outcome, Outcome::Allow);
1206 assert_eq!(evaluation.matched, None);
1207 assert_eq!(evaluation.warned.len(), 1);
1208 assert_eq!(evaluation.warned[0].name, "would-deny");
1209 assert_eq!(evaluation.warned[0].then, Effect::Deny);
1210 }
1211
1212 #[tokio::test]
1213 async fn the_enforcing_twin_of_a_warn_rule_denies() {
1214 let (check, _) = StubCheck::passing();
1215 let policy = FilterPolicy::new(
1216 vec![("yes".to_string(), check)],
1217 vec![rule("deny-it", "yes", Effect::Deny)],
1218 Effect::Allow,
1219 ProxyPolicy::default(),
1220 );
1221
1222 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1223 }
1224
1225 #[tokio::test]
1228 async fn an_unknown_rule_poisons_a_differing_answer() {
1229 let (down, _) = StubCheck::undecided();
1230 let policy = FilterPolicy::new(
1231 vec![("inventory".to_string(), down)],
1232 vec![rule("inventory-owned", "inventory", Effect::Allow)],
1233 Effect::Deny,
1234 ProxyPolicy::default(),
1235 );
1236
1237 assert!(matches!(decide(&policy).await, Outcome::Undecided(_)));
1240 }
1241
1242 #[tokio::test]
1245 async fn an_unknown_rule_is_harmless_when_the_answer_agrees() {
1246 let (down, _) = StubCheck::undecided();
1247 let (up, _) = StubCheck::passing();
1248 let policy = FilterPolicy::new(
1249 vec![("inventory".to_string(), down), ("mgmt".to_string(), up)],
1250 vec![
1251 rule("inventory-owned", "inventory", Effect::Allow),
1252 rule("mgmt-bypass", "mgmt", Effect::Allow),
1253 ],
1254 Effect::Deny,
1255 ProxyPolicy::default(),
1256 );
1257
1258 assert_eq!(decide(&policy).await, Outcome::Allow);
1259 }
1260
1261 #[tokio::test]
1264 async fn an_or_absorbs_the_outage_within_a_single_rule() {
1265 let (down, _) = StubCheck::undecided();
1266 let (up, _) = StubCheck::passing();
1267 let policy = FilterPolicy::new(
1268 vec![("inventory".to_string(), down), ("mgmt".to_string(), up)],
1269 vec![rule("reachable", "mgmt or inventory", Effect::Allow)],
1270 Effect::Deny,
1271 ProxyPolicy::default(),
1272 );
1273
1274 assert_eq!(decide(&policy).await, Outcome::Allow);
1275 }
1276
1277 #[tokio::test]
1280 async fn the_inventory_alone_is_still_an_outage() {
1281 let (down, _) = StubCheck::undecided();
1282 let policy = FilterPolicy::new(
1283 vec![("inventory".to_string(), down)],
1284 vec![rule("reachable", "inventory", Effect::Allow)],
1285 Effect::Deny,
1286 ProxyPolicy::default(),
1287 );
1288
1289 assert!(matches!(decide(&policy).await, Outcome::Undecided(_)));
1290 }
1291
1292 #[tokio::test]
1293 async fn a_warn_rule_that_cannot_be_evaluated_poisons_nothing() {
1294 let (down, _) = StubCheck::undecided();
1295 let mut dry_run = rule("inventory-owned", "inventory", Effect::Allow);
1296 dry_run.mode = Mode::Warn;
1297
1298 let policy = FilterPolicy::new(
1299 vec![("inventory".to_string(), down)],
1300 vec![dry_run],
1301 Effect::Deny,
1302 ProxyPolicy::default(),
1303 );
1304
1305 assert!(matches!(decide(&policy).await, Outcome::Deny(_)));
1306 }
1307
1308 #[tokio::test]
1311 async fn a_rule_message_is_shown_verbatim() {
1312 let (check, _) = StubCheck::passing();
1313 let mut denying = rule("no-tenants", "yes", Effect::Deny);
1314 denying.message = Some("this address owns no such name".to_string());
1315
1316 let policy = FilterPolicy::new(
1317 vec![("yes".to_string(), check)],
1318 vec![denying],
1319 Effect::Allow,
1320 ProxyPolicy::default(),
1321 );
1322
1323 assert_eq!(
1324 decide(&policy).await,
1325 Outcome::Deny("this address owns no such name".to_string())
1326 );
1327 }
1328
1329 #[tokio::test]
1330 async fn a_denying_rule_without_a_message_names_itself() {
1331 let (check, _) = StubCheck::passing();
1332 let policy = FilterPolicy::new(
1333 vec![("yes".to_string(), check)],
1334 vec![rule("no-tenants", "yes", Effect::Deny)],
1335 Effect::Allow,
1336 ProxyPolicy::default(),
1337 );
1338
1339 assert_eq!(
1340 decide(&policy).await,
1341 Outcome::Deny("refused by policy rule `no-tenants`".to_string())
1342 );
1343 }
1344
1345 #[tokio::test]
1346 async fn falling_through_to_the_default_reports_the_first_refusing_check() {
1347 let (check, _) = StubCheck::failing();
1348 let policy = FilterPolicy::new(
1349 vec![("addr".to_string(), check)],
1350 vec![rule("permitted", "addr", Effect::Allow)],
1351 Effect::Deny,
1352 ProxyPolicy::default(),
1353 );
1354
1355 assert_eq!(
1356 decide(&policy).await,
1357 Outcome::Deny("stub refused".to_string())
1358 );
1359 }
1360
1361 #[tokio::test]
1365 async fn a_default_deny_quotes_the_last_rule_not_the_first_bypass() {
1366 let (bypass, _) = StubCheck::with(
1367 Verdict::Fail("path /newOrder is not allowed".to_string()),
1368 StageSet::both(),
1369 );
1370 let (main, _) = StubCheck::with(
1371 Verdict::Fail("address 203.0.113.9 is not allowed".to_string()),
1372 StageSet::both(),
1373 );
1374
1375 let policy = FilterPolicy::new(
1376 vec![
1377 ("public-paths".to_string(), bypass),
1378 ("mgmt-net".to_string(), main),
1379 ],
1380 vec![
1381 rule("public", "public-paths", Effect::Allow),
1382 rule("mgmt-bypass", "mgmt-net", Effect::Allow),
1383 ],
1384 Effect::Deny,
1385 ProxyPolicy::default(),
1386 );
1387
1388 assert_eq!(
1389 decide(&policy).await,
1390 Outcome::Deny("address 203.0.113.9 is not allowed".to_string())
1391 );
1392 }
1393
1394 #[tokio::test]
1397 async fn a_default_deny_widens_when_the_last_rule_refused_nothing() {
1398 let (failing, _) = StubCheck::failing();
1399 let (passing, _) = StubCheck::passing();
1400
1401 let policy = FilterPolicy::new(
1402 vec![
1403 ("first".to_string(), failing),
1404 ("second".to_string(), passing),
1405 ],
1406 vec![
1407 rule("early", "first", Effect::Allow),
1408 rule("late", "not second", Effect::Allow),
1411 ],
1412 Effect::Deny,
1413 ProxyPolicy::default(),
1414 );
1415
1416 assert_eq!(
1417 decide(&policy).await,
1418 Outcome::Deny("stub refused".to_string())
1419 );
1420 }
1421
1422 #[tokio::test]
1423 async fn a_default_deny_with_nothing_to_report_says_so() {
1424 let (check, _) = StubCheck::passing();
1425 let policy = FilterPolicy::new(
1430 vec![("yes".to_string(), check)],
1431 vec![rule("permitted", "not yes", Effect::Allow)],
1432 Effect::Deny,
1433 ProxyPolicy::default(),
1434 );
1435
1436 let evaluation = policy.evaluate_connection(&connection_context()).await;
1437 assert_eq!(
1438 evaluation.outcome,
1439 Outcome::Deny("no policy rule permits this request".to_string())
1440 );
1441 assert_eq!(evaluation.checks.len(), 1);
1442 assert_eq!(evaluation.checks[0].verdict, Verdict::Pass);
1443 }
1444
1445 #[tokio::test]
1448 async fn the_trace_records_every_evaluated_check_in_order() {
1449 let (first, _) = StubCheck::passing();
1450 let (second, _) = StubCheck::failing();
1451 let policy = FilterPolicy::new(
1452 vec![("first".to_string(), first), ("second".to_string(), second)],
1453 vec![rule("r", "first and second", Effect::Allow)],
1454 Effect::Deny,
1455 ProxyPolicy::default(),
1456 );
1457
1458 let evaluation = policy.evaluate_connection(&connection_context()).await;
1459 let names: Vec<&str> = evaluation
1460 .checks
1461 .iter()
1462 .map(|outcome| outcome.name.as_str())
1463 .collect();
1464 assert_eq!(names, vec!["first", "second"]);
1465 assert_eq!(evaluation.checks[0].kind, "stub");
1466 }
1467
1468 #[test]
1471 fn a_rule_takes_the_intersection_of_its_checks_stages() {
1472 let (anywhere, _) = StubCheck::with(Verdict::Pass, StageSet::both());
1473 let (names_only, _) = StubCheck::with(Verdict::Pass, StageSet::identifiers_only());
1474 let policy = FilterPolicy::new(
1475 vec![
1476 ("anywhere".to_string(), anywhere),
1477 ("names".to_string(), names_only),
1478 ],
1479 vec![rule("mixed", "anywhere or names", Effect::Allow)],
1480 Effect::Deny,
1481 ProxyPolicy::default(),
1482 );
1483
1484 assert!(!policy.has_rules_at(Stage::Connection));
1485 assert!(policy.has_rules_at(Stage::Identifiers));
1486 }
1487
1488 #[test]
1489 fn a_rule_naming_an_unknown_check_is_never_applicable() {
1490 let policy = FilterPolicy::new(
1491 Vec::new(),
1492 vec![rule("broken", "nonexistent", Effect::Allow)],
1493 Effect::Deny,
1494 ProxyPolicy::default(),
1495 );
1496
1497 assert!(!policy.has_rules_at(Stage::Connection));
1498 assert!(!policy.has_rules_at(Stage::Identifiers));
1499 assert!(policy.is_active());
1500 }
1501
1502 #[test]
1503 fn stage_sets_describe_themselves() {
1504 assert_eq!(StageSet::both().to_string(), "connection and identifiers");
1505 assert_eq!(StageSet::connection_only().to_string(), "connection only");
1506 assert_eq!(StageSet::identifiers_only().to_string(), "identifiers only");
1507 assert_eq!(StageSet::none().to_string(), "no stage");
1508 assert!(StageSet::none().is_empty());
1509 assert!(
1510 StageSet::connection_only()
1511 .intersect(StageSet::identifiers_only())
1512 .is_empty()
1513 );
1514 assert_eq!(Stage::Connection.as_str(), "connection");
1515 assert_eq!(Stage::Identifiers.as_str(), "identifiers");
1516 assert_eq!(Effect::Allow.as_str(), "allow");
1517 assert_eq!(Effect::Deny.as_str(), "deny");
1518 }
1519
1520 #[test]
1521 fn the_default_policy_decides_nothing() {
1522 let policy = FilterPolicy::default();
1523 assert!(!policy.is_active());
1524 assert!(!policy.has_rules_at(Stage::Connection));
1525 assert_eq!(policy.default_effect(), Effect::Deny);
1526 assert!(format!("{policy:?}").contains("FilterPolicy"));
1527 }
1528
1529 #[test]
1530 fn the_debug_rendering_names_checks_and_rules() {
1531 let (check, _) = StubCheck::passing();
1532 let policy = FilterPolicy::new(
1533 vec![("mgmt".to_string(), check)],
1534 vec![rule("bypass", "mgmt", Effect::Allow)],
1535 Effect::Deny,
1536 ProxyPolicy::default(),
1537 );
1538
1539 let rendered = format!("{policy:?}");
1540 assert!(rendered.contains("mgmt: stub"), "{rendered}");
1541 assert!(rendered.contains("bypass: mgmt -> allow"), "{rendered}");
1542 }
1543}