faucet_core/policy/
compile.rs1use super::spec::{PolicyRule, PolicySpec};
5use crate::masking::{Detector, detect};
6use regex::Regex;
7use std::collections::{BTreeSet, HashSet};
8
9#[derive(Debug, Clone)]
11pub struct CompiledClassification {
12 pub label: String,
13 pub fields: HashSet<String>,
14 pub field_pattern: Option<Regex>,
15 pub value_detector: Option<Detector>,
16}
17
18#[derive(Debug, Clone)]
20pub struct CompiledPolicy {
21 classifications: Vec<CompiledClassification>,
22 rules: Vec<PolicyRule>,
23}
24
25impl CompiledPolicy {
26 pub fn compile(spec: &PolicySpec) -> Result<Self, String> {
28 spec.validate()?;
29 let classifications = spec
30 .classifications
31 .iter()
32 .map(|c| {
33 Ok(CompiledClassification {
34 label: c.label.clone(),
35 fields: c.fields.iter().cloned().collect(),
36 field_pattern: match &c.field_pattern {
37 Some(p) => Some(Regex::new(p).map_err(|e| e.to_string())?),
38 None => None,
39 },
40 value_detector: c.value_detector,
41 })
42 })
43 .collect::<Result<Vec<_>, String>>()?;
44 Ok(Self {
45 classifications,
46 rules: spec.rules.clone(),
47 })
48 }
49
50 pub fn rules(&self) -> &[PolicyRule] {
51 &self.rules
52 }
53
54 pub fn classifications(&self) -> &[CompiledClassification] {
55 &self.classifications
56 }
57
58 pub fn labels_for_name(&self, name: &str) -> BTreeSet<String> {
62 let leaf = name.rsplit('.').next().unwrap_or(name);
63 self.classifications
64 .iter()
65 .filter(|c| {
66 c.fields.contains(name)
67 || c.fields.contains(leaf)
68 || c.field_pattern.as_ref().is_some_and(|re| re.is_match(name))
69 })
70 .map(|c| c.label.clone())
71 .collect()
72 }
73
74 pub fn labels_for_value(&self, value: &str) -> BTreeSet<String> {
76 self.classifications
77 .iter()
78 .filter(|c| c.value_detector.is_some_and(|d| detect::detects(d, value)))
79 .map(|c| c.label.clone())
80 .collect()
81 }
82
83 pub fn value_detectors(&self) -> impl Iterator<Item = (&str, Detector)> {
85 self.classifications
86 .iter()
87 .filter_map(|c| c.value_detector.map(|d| (c.label.as_str(), d)))
88 }
89
90 pub fn governs(&self, label: &str) -> bool {
92 self.rules.iter().any(|r| r.when.label == label)
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use serde_json::json;
100
101 fn policy() -> CompiledPolicy {
102 let spec: PolicySpec = serde_json::from_value(json!({
103 "classifications": [
104 {"label": "pii", "fields": ["email", "user.phone"], "value_detector": "email"},
105 {"label": "pii", "field_pattern": "(?i)ssn$", "value_detector": "ssn"},
106 {"label": "finance", "field_pattern": "^amount"}
107 ],
108 "rules": [
109 {"name": "r", "when": {"label": "pii"}, "deny": true}
110 ]
111 }))
112 .unwrap();
113 CompiledPolicy::compile(&spec).unwrap()
114 }
115
116 #[test]
117 fn labels_by_name_and_pattern() {
118 let p = policy();
119 assert_eq!(p.labels_for_name("email"), ["pii".to_string()].into());
120 assert_eq!(p.labels_for_name("user.phone"), ["pii".to_string()].into());
121 assert_eq!(
122 p.labels_for_name("contact.email"),
123 ["pii".to_string()].into(),
124 "a bare field name covers the leaf key"
125 );
126 assert_eq!(
127 p.labels_for_name("customer_SSN"),
128 ["pii".to_string()].into()
129 );
130 assert_eq!(
131 p.labels_for_name("amount_cents"),
132 ["finance".to_string()].into()
133 );
134 assert!(p.labels_for_name("id").is_empty());
135 }
136
137 #[test]
138 fn labels_by_value_use_the_masking_detectors() {
139 let p = policy();
140 assert_eq!(p.labels_for_value("a@b.io"), ["pii".to_string()].into());
141 assert_eq!(
142 p.labels_for_value("123-45-6789"),
143 ["pii".to_string()].into()
144 );
145 assert!(p.labels_for_value("hello").is_empty());
146 assert_eq!(p.value_detectors().count(), 2);
147 assert!(p.governs("pii") && !p.governs("finance"));
148 assert_eq!(p.rules().len(), 1);
149 assert_eq!(p.classifications().len(), 3);
150 }
151
152 #[test]
153 fn compile_rejects_an_invalid_spec() {
154 let spec: PolicySpec = serde_json::from_value(json!({
155 "classifications": [{"label": "x", "field_pattern": "("}],
156 "rules": []
157 }))
158 .unwrap();
159 assert!(CompiledPolicy::compile(&spec).is_err());
160 }
161}