qubit_redact/policy/field/
redaction_rules.rs1use std::ops::ControlFlow;
11use std::sync::Arc;
12
13use super::AllowRule;
14use super::FieldClassification;
15use super::FieldMatchKind;
16use super::FieldNameMatching;
17use super::RedactionFloor;
18use super::SensitiveFieldRule;
19use super::Sensitivity;
20use super::UnknownFieldPolicy;
21use crate::policy::ResolvedField;
22use crate::policy::internal::RedactionPolicyInner;
23use crate::policy::internal::visit_canonical_field_candidates;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct RedactionRules {
39 application: Arc<RedactionPolicyInner>,
41 floor: Option<RedactionFloor>,
43}
44
45impl RedactionRules {
46 #[must_use]
48 #[inline(always)]
49 pub(crate) fn new(application: RedactionPolicyInner, floor: Option<RedactionFloor>) -> Self {
50 Self {
51 application: Arc::new(application),
52 floor,
53 }
54 }
55
56 #[must_use]
58 #[inline(always)]
59 pub fn floor(&self) -> Option<&RedactionFloor> {
60 self.floor.as_ref()
61 }
62
63 #[must_use]
65 #[inline(always)]
66 pub fn sensitivity_for(&self, field: &str) -> Option<Sensitivity> {
67 match self.resolve_field(field) {
68 ResolvedField::Sensitive { sensitivity } => Some(sensitivity),
69 ResolvedField::PassThrough => None,
70 }
71 }
72
73 #[must_use]
75 #[inline(always)]
76 pub fn matching(&self) -> FieldNameMatching {
77 self.application.matching
78 }
79
80 #[must_use]
82 #[inline(always)]
83 pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
84 self.application.unknown_field_policy
85 }
86
87 pub fn application_sensitive_rules(&self) -> impl Iterator<Item = SensitiveFieldRule<'_>> {
89 self.application
90 .sensitive
91 .iter()
92 .map(|(field, level)| SensitiveFieldRule::new(field, *level))
93 }
94
95 pub fn application_allow_rules(&self) -> impl Iterator<Item = AllowRule<'_>> {
97 self.application
98 .allow_exact
99 .iter()
100 .map(|field| AllowRule::new(field, FieldNameMatching::Exact))
101 .chain(
102 self.application
103 .allow_suffix
104 .iter()
105 .map(|field| AllowRule::new(field, FieldNameMatching::ExactOrTokenSuffix)),
106 )
107 }
108
109 #[must_use]
111 #[inline(always)]
112 pub fn with_floor(mut self, floor: RedactionFloor) -> Self {
113 self.floor = Some(floor);
114 self
115 }
116
117 #[must_use]
119 #[inline(always)]
120 pub fn add_floor(mut self, floor: RedactionFloor) -> Self {
121 self.floor = Some(match self.floor.take() {
122 Some(existing) => existing.combine(&floor),
123 None => floor,
124 });
125 self
126 }
127
128 #[must_use]
135 #[inline(always)]
136 pub fn disable_floor(mut self) -> Self {
137 self.floor = None;
138 self
139 }
140
141 #[must_use]
144 #[inline(always)]
145 pub fn classify_field<'a>(&'a self, field: &str) -> FieldClassification<'a> {
146 classify_inner(&self.application, field, self.application.matching, true)
147 }
148
149 #[must_use]
151 #[inline(always)]
152 pub(crate) fn sensitivity_for_exact(&self, field: &str) -> Option<Sensitivity> {
153 match self.resolve_field_exact(field) {
154 ResolvedField::Sensitive { sensitivity } => Some(sensitivity),
155 ResolvedField::PassThrough => None,
156 }
157 }
158
159 pub(crate) fn resolve_field_exact(&self, field: &str) -> ResolvedField {
161 let application = sensitivity_inner(&self.application, field, FieldNameMatching::Exact, true);
162 let floor = self
163 .floor
164 .as_ref()
165 .and_then(|floor| sensitivity_inner(&floor.inner, field, FieldNameMatching::Exact, false));
166 match self.floor.as_ref().zip(floor) {
167 Some((_floor, floor_level)) => ResolvedField::Sensitive {
168 sensitivity: application.map_or(floor_level, |level| level.max(floor_level)),
169 },
170 None => match application {
171 Some(sensitivity) => ResolvedField::Sensitive { sensitivity },
172 None => ResolvedField::PassThrough,
173 },
174 }
175 }
176
177 #[inline(always)]
179 pub(crate) fn resolve_field(&self, field: &str) -> ResolvedField {
180 self.resolve_field_with_matching(field, self.application.matching)
181 }
182
183 #[inline(always)]
185 pub(crate) fn clone_application(&self) -> RedactionPolicyInner {
186 (*self.application).clone()
187 }
188
189 fn resolve_field_with_matching(&self, field: &str, matching: FieldNameMatching) -> ResolvedField {
191 let application = sensitivity_inner(&self.application, field, matching, true);
192 let floor = self
193 .floor
194 .as_ref()
195 .and_then(|floor| sensitivity_inner(&floor.inner, field, floor.inner.matching, false));
196 match floor {
197 Some(floor_level) => ResolvedField::Sensitive {
198 sensitivity: application.map_or(floor_level, |level| level.max(floor_level)),
199 },
200 None => match application {
201 Some(sensitivity) => ResolvedField::Sensitive { sensitivity },
202 None => ResolvedField::PassThrough,
203 },
204 }
205 }
206}
207
208#[must_use]
210fn classify_inner<'a>(
211 inner: &'a RedactionPolicyInner,
212 field: &str,
213 matching: FieldNameMatching,
214 allow: bool,
215) -> FieldClassification<'a> {
216 match visit_canonical_field_candidates(field, matching, |is_exact, candidate| {
217 let match_kind = if is_exact {
218 FieldMatchKind::Exact
219 } else {
220 FieldMatchKind::TokenSuffix
221 };
222 if allow
223 && is_exact
224 && let Some(field) = inner.allow_exact.get(candidate)
225 {
226 return ControlFlow::Break(FieldClassification::Allowed {
227 rule: AllowRule::new(field, FieldNameMatching::Exact),
228 match_kind,
229 });
230 }
231 if allow && let Some(field) = inner.allow_suffix.get(candidate) {
232 return ControlFlow::Break(FieldClassification::Allowed {
233 rule: AllowRule::new(field, FieldNameMatching::ExactOrTokenSuffix),
234 match_kind,
235 });
236 }
237 if let Some((field, sensitivity)) = inner.sensitive.get_key_value(candidate) {
238 return ControlFlow::Break(FieldClassification::Sensitive {
239 rule: SensitiveFieldRule::new(field, *sensitivity),
240 match_kind,
241 });
242 }
243 ControlFlow::Continue(())
244 }) {
245 ControlFlow::Break(classification) => classification,
246 ControlFlow::Continue(()) => FieldClassification::Unknown,
247 }
248}
249
250#[must_use]
252fn sensitivity_inner(
253 inner: &RedactionPolicyInner,
254 field: &str,
255 matching: FieldNameMatching,
256 allow: bool,
257) -> Option<Sensitivity> {
258 match classify_inner(inner, field, matching, allow) {
259 FieldClassification::Allowed { .. } => None,
260 FieldClassification::Sensitive { .. } | FieldClassification::Unknown => {
261 strongest_sensitive_match(inner, field, matching).or_else(|| inner.unknown_field_policy.sensitivity())
262 }
263 }
264}
265
266#[must_use]
268fn strongest_sensitive_match(
269 inner: &RedactionPolicyInner,
270 field: &str,
271 matching: FieldNameMatching,
272) -> Option<Sensitivity> {
273 let mut strongest: Option<Sensitivity> = None;
274 let _ = visit_canonical_field_candidates(field, matching, |_is_exact, candidate| {
275 if let Some(level) = inner.sensitive.get(candidate) {
276 strongest = Some(strongest.map_or(*level, |current| current.max(*level)));
277 }
278 ControlFlow::<()>::Continue(())
279 });
280 strongest
281}