krabka_replicator/
residency.rs1use crate::{
7 config::{PolicyConfig, Residency},
8 error::ReplicatorError,
9 selector::Selector,
10};
11
12struct Rule {
13 topics: Selector,
14 residency: Residency,
15}
16
17pub struct ResidencyGate {
19 rules: Vec<Rule>,
20}
21
22impl ResidencyGate {
23 pub fn compile(policies: &[PolicyConfig]) -> Result<Self, ReplicatorError> {
32 let mut rules = Vec::new();
33 for p in policies {
34 if let Some(res) = &p.residency {
35 rules.push(Rule {
36 topics: Selector::compile(&p.topics, &[])?,
37 residency: res.clone(),
38 });
39 }
40 }
41 Ok(Self { rules })
42 }
43
44 #[must_use]
52 pub fn permits(&self, topic: &str, target_zones: &[String]) -> bool {
53 for rule in &self.rules {
54 if !rule.topics.matches(topic) {
55 continue;
56 }
57 if rule
59 .residency
60 .deny_zones
61 .iter()
62 .any(|z| target_zones.contains(z))
63 {
64 return false;
65 }
66 if !rule.residency.allow_zones.is_empty()
68 && !rule
69 .residency
70 .allow_zones
71 .iter()
72 .any(|z| target_zones.contains(z))
73 {
74 return false;
75 }
76 }
77 true
78 }
79}
80
81#[cfg(test)]
82mod tests {
83
84 use super::*;
85 use crate::config::{PolicyConfig, Residency};
86
87 fn policies() -> Vec<PolicyConfig> {
88 vec![PolicyConfig {
89 name: "keep-pii-in-eu".into(),
90 topics: vec!["customers".into(), "kyc.*".into()],
91 residency: Some(Residency {
92 allow_zones: vec!["gdpr".into()],
93 deny_zones: vec![],
94 }),
95 }]
96 }
97
98 #[test]
99 fn allows_when_target_zone_in_allow_list() {
100 let gate = ResidencyGate::compile(&policies()).unwrap();
101 assert2::assert!(gate.permits("customers", &["eu".into(), "gdpr".into()]));
102 }
103
104 #[test]
105 fn blocks_when_target_lacks_allowed_zone() {
106 let gate = ResidencyGate::compile(&policies()).unwrap();
107 assert2::assert!(!gate.permits("customers", &["us".into()]));
108 assert2::assert!(!gate.permits("kyc.eu", &["us".into()]));
109 }
110
111 #[test]
112 fn deny_wins_over_allow() {
113 let mut p = policies();
114 p[0].residency = Some(Residency {
115 allow_zones: vec!["gdpr".into()],
116 deny_zones: vec!["gdpr".into()],
117 });
118 let gate = ResidencyGate::compile(&p).unwrap();
119 assert2::assert!(!gate.permits("customers", &["gdpr".into()]));
120 }
121
122 #[test]
123 fn unmatched_topic_is_permitted() {
124 let gate = ResidencyGate::compile(&policies()).unwrap();
125 assert2::assert!(gate.permits("orders", &["us".into()]));
126 }
127}