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)]
27pub struct RedactionRules {
28 application: Arc<RedactionPolicyInner>,
30 floor: Option<RedactionFloor>,
32}
33
34impl RedactionRules {
35 #[must_use]
37 pub(crate) fn new(application: RedactionPolicyInner, floor: Option<RedactionFloor>) -> Self {
38 Self {
39 application: Arc::new(application),
40 floor,
41 }
42 }
43
44 #[must_use]
46 #[inline]
47 pub fn floor(&self) -> Option<&RedactionFloor> {
48 self.floor.as_ref()
49 }
50
51 #[must_use]
53 #[inline]
54 pub fn with_floor(mut self, floor: RedactionFloor) -> Self {
55 self.floor = Some(floor);
56 self
57 }
58
59 #[must_use]
66 pub fn disable_floor(mut self) -> Self {
67 self.floor = None;
68 self
69 }
70
71 #[must_use]
74 pub fn classify_field<'a>(&'a self, field: &str) -> FieldClassification<'a> {
75 classify_inner(&self.application, field, self.application.matching, true)
76 }
77
78 #[must_use]
80 #[inline]
81 pub fn sensitivity_for(&self, field: &str) -> Option<Sensitivity> {
82 match self.resolve_field(field) {
83 ResolvedField::Sensitive { sensitivity } => Some(sensitivity),
84 ResolvedField::PassThrough => None,
85 }
86 }
87
88 #[must_use]
90 #[inline]
91 pub(crate) fn sensitivity_for_exact(&self, field: &str) -> Option<Sensitivity> {
92 match self.resolve_field_exact(field) {
93 ResolvedField::Sensitive { sensitivity } => Some(sensitivity),
94 ResolvedField::PassThrough => None,
95 }
96 }
97
98 pub(crate) fn resolve_field_exact(&self, field: &str) -> ResolvedField {
100 let application = sensitivity_inner(&self.application, field, FieldNameMatching::Exact, true);
101 let floor = self
102 .floor
103 .as_ref()
104 .and_then(|floor| sensitivity_inner(&floor.inner, field, FieldNameMatching::Exact, false));
105 match self.floor.as_ref().zip(floor) {
106 Some((_floor, floor_level)) => ResolvedField::Sensitive {
107 sensitivity: application.map_or(floor_level, |level| level.max(floor_level)),
108 },
109 None => match application {
110 Some(sensitivity) => ResolvedField::Sensitive { sensitivity },
111 None => ResolvedField::PassThrough,
112 },
113 }
114 }
115
116 #[inline]
118 pub(crate) fn resolve_field(&self, field: &str) -> ResolvedField {
119 self.resolve_field_with_matching(field, self.application.matching)
120 }
121
122 fn resolve_field_with_matching(&self, field: &str, matching: FieldNameMatching) -> ResolvedField {
124 let application = sensitivity_inner(&self.application, field, matching, true);
125 let floor = self
126 .floor
127 .as_ref()
128 .and_then(|floor| sensitivity_inner(&floor.inner, field, floor.inner.matching, false));
129 match floor {
130 Some(floor_level) => ResolvedField::Sensitive {
131 sensitivity: application.map_or(floor_level, |level| level.max(floor_level)),
132 },
133 None => match application {
134 Some(sensitivity) => ResolvedField::Sensitive { sensitivity },
135 None => ResolvedField::PassThrough,
136 },
137 }
138 }
139
140 #[must_use]
142 #[inline]
143 pub fn matching(&self) -> FieldNameMatching {
144 self.application.matching
145 }
146
147 #[must_use]
149 #[inline]
150 pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
151 self.application.unknown_field_policy
152 }
153
154 pub fn application_sensitive_rules(&self) -> impl Iterator<Item = SensitiveFieldRule<'_>> {
156 self.application
157 .sensitive
158 .iter()
159 .map(|(field, level)| SensitiveFieldRule::new(field, *level))
160 }
161
162 pub fn application_allow_rules(&self) -> impl Iterator<Item = AllowRule<'_>> {
164 self.application
165 .allow_exact
166 .iter()
167 .map(|field| AllowRule::new(field, FieldNameMatching::Exact))
168 .chain(
169 self.application
170 .allow_suffix
171 .iter()
172 .map(|field| AllowRule::new(field, FieldNameMatching::ExactOrTokenSuffix)),
173 )
174 }
175
176 pub(crate) fn clone_application(&self) -> RedactionPolicyInner {
178 (*self.application).clone()
179 }
180}
181
182#[must_use]
184fn classify_inner<'a>(
185 inner: &'a RedactionPolicyInner,
186 field: &str,
187 matching: FieldNameMatching,
188 allow: bool,
189) -> FieldClassification<'a> {
190 match visit_canonical_field_candidates(field, matching, |is_exact, candidate| {
191 let match_kind = if is_exact {
192 FieldMatchKind::Exact
193 } else {
194 FieldMatchKind::TokenSuffix
195 };
196 if allow
197 && is_exact
198 && let Some(field) = inner.allow_exact.get(candidate)
199 {
200 return ControlFlow::Break(FieldClassification::Allowed {
201 rule: AllowRule::new(field, FieldNameMatching::Exact),
202 match_kind,
203 });
204 }
205 if allow && let Some(field) = inner.allow_suffix.get(candidate) {
206 return ControlFlow::Break(FieldClassification::Allowed {
207 rule: AllowRule::new(field, FieldNameMatching::ExactOrTokenSuffix),
208 match_kind,
209 });
210 }
211 if let Some((field, sensitivity)) = inner.sensitive.get_key_value(candidate) {
212 return ControlFlow::Break(FieldClassification::Sensitive {
213 rule: SensitiveFieldRule::new(field, *sensitivity),
214 match_kind,
215 });
216 }
217 ControlFlow::Continue(())
218 }) {
219 ControlFlow::Break(classification) => classification,
220 ControlFlow::Continue(()) => FieldClassification::Unknown,
221 }
222}
223
224#[must_use]
226fn sensitivity_inner(
227 inner: &RedactionPolicyInner,
228 field: &str,
229 matching: FieldNameMatching,
230 allow: bool,
231) -> Option<Sensitivity> {
232 match classify_inner(inner, field, matching, allow) {
233 FieldClassification::Allowed { .. } => None,
234 FieldClassification::Sensitive { .. } | FieldClassification::Unknown => {
235 strongest_sensitive_match(inner, field, matching).or_else(|| inner.unknown_field_policy.sensitivity())
236 }
237 }
238}
239
240#[must_use]
242fn strongest_sensitive_match(
243 inner: &RedactionPolicyInner,
244 field: &str,
245 matching: FieldNameMatching,
246) -> Option<Sensitivity> {
247 let mut strongest: Option<Sensitivity> = None;
248 let _ = visit_canonical_field_candidates(field, matching, |_is_exact, candidate| {
249 if let Some(level) = inner.sensitive.get(candidate) {
250 strongest = Some(strongest.map_or(*level, |current| current.max(*level)));
251 }
252 ControlFlow::<()>::Continue(())
253 });
254 strongest
255}