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 labels_for_scalar(&self, value: &serde_json::Value) -> BTreeSet<String> {
86 self.classifications
87 .iter()
88 .filter(|c| {
89 c.value_detector
90 .is_some_and(|d| detect::detects_value(d, value))
91 })
92 .map(|c| c.label.clone())
93 .collect()
94 }
95
96 pub fn value_detectors(&self) -> impl Iterator<Item = (&str, Detector)> {
98 self.classifications
99 .iter()
100 .filter_map(|c| c.value_detector.map(|d| (c.label.as_str(), d)))
101 }
102
103 pub fn governs(&self, label: &str) -> bool {
105 self.rules.iter().any(|r| r.when.label == label)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use serde_json::json;
113
114 fn policy() -> CompiledPolicy {
115 let spec: PolicySpec = serde_json::from_value(json!({
116 "classifications": [
117 {"label": "pii", "fields": ["email", "user.phone"], "value_detector": "email"},
118 {"label": "pii", "field_pattern": "(?i)ssn$", "value_detector": "ssn"},
119 {"label": "finance", "field_pattern": "^amount"}
120 ],
121 "rules": [
122 {"name": "r", "when": {"label": "pii"}, "deny": true}
123 ]
124 }))
125 .unwrap();
126 CompiledPolicy::compile(&spec).unwrap()
127 }
128
129 #[test]
130 fn labels_by_name_and_pattern() {
131 let p = policy();
132 assert_eq!(p.labels_for_name("email"), ["pii".to_string()].into());
133 assert_eq!(p.labels_for_name("user.phone"), ["pii".to_string()].into());
134 assert_eq!(
135 p.labels_for_name("contact.email"),
136 ["pii".to_string()].into(),
137 "a bare field name covers the leaf key"
138 );
139 assert_eq!(
140 p.labels_for_name("customer_SSN"),
141 ["pii".to_string()].into()
142 );
143 assert_eq!(
144 p.labels_for_name("amount_cents"),
145 ["finance".to_string()].into()
146 );
147 assert!(p.labels_for_name("id").is_empty());
148 }
149
150 #[test]
151 fn labels_by_value_use_the_masking_detectors() {
152 let p = policy();
153 assert_eq!(p.labels_for_value("a@b.io"), ["pii".to_string()].into());
154 assert_eq!(
155 p.labels_for_value("123-45-6789"),
156 ["pii".to_string()].into()
157 );
158 assert!(p.labels_for_value("hello").is_empty());
159 assert_eq!(p.value_detectors().count(), 2);
160 assert!(p.governs("pii") && !p.governs("finance"));
161 assert_eq!(p.rules().len(), 1);
162 assert_eq!(p.classifications().len(), 3);
163 }
164
165 #[test]
166 fn compile_rejects_an_invalid_spec() {
167 let spec: PolicySpec = serde_json::from_value(json!({
168 "classifications": [{"label": "x", "field_pattern": "("}],
169 "rules": []
170 }))
171 .unwrap();
172 assert!(CompiledPolicy::compile(&spec).is_err());
173 }
174}