qubit_redact/policy/redaction_policy.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Immutable field-classification, masking, and diagnostic policy.
9
10use std::sync::Arc;
11use std::sync::LazyLock;
12
13use super::AllowRule;
14use super::FieldClassification;
15use super::FieldNameMatching;
16use super::MaskingPolicy;
17use super::RedactionFloor;
18use super::RedactionPolicyBuilder;
19use super::RedactionRules;
20use super::SensitiveFieldRule;
21use super::Sensitivity;
22#[cfg(feature = "json")]
23use super::UnkeyedJsonValuePolicy;
24use super::UnknownFieldPolicy;
25use super::internal::RedactionPolicyInner;
26use super::redaction_limits::RedactionLimits;
27
28/// Built-in sensitive fields not owned by a named preset.
29pub(super) const STANDARD_EXTRA_FIELDS: &[(&str, Sensitivity)] = &[
30 ("auth_app_token", Sensitivity::High),
31 ("auth_user_token", Sensitivity::High),
32 ("connection_string", Sensitivity::Secret),
33 ("database_uri", Sensitivity::Secret),
34 ("database_url", Sensitivity::Secret),
35 ("license_key", Sensitivity::Medium),
36 ("mysql_pwd", Sensitivity::Secret),
37 ("rediscli_auth", Sensitivity::Secret),
38 ("sig", Sensitivity::Secret),
39 ("signature", Sensitivity::Secret),
40];
41
42/// Lazily initialized fixed standard policy.
43static STANDARD_POLICY: LazyLock<RedactionPolicy> = LazyLock::new(|| {
44 RedactionPolicy::from_rules(
45 RedactionRules::new(
46 RedactionPolicyInner {
47 sensitive: Default::default(),
48 allow_exact: Default::default(),
49 allow_suffix: Default::default(),
50 matching: FieldNameMatching::ExactOrTokenSuffix,
51 unknown_field_policy: UnknownFieldPolicy::PassThrough,
52 },
53 Some(RedactionFloor::standard()),
54 ),
55 MaskingPolicy::default(),
56 RedactionLimits::default(),
57 #[cfg(feature = "http")]
58 crate::formats::http::HttpPolicyBuilder::new()
59 .build()
60 .expect("the built-in HTTP policy must be valid"),
61 #[cfg(feature = "uri")]
62 crate::formats::uri::UriPolicyBuilder::new()
63 .build()
64 .expect("the built-in URI policy must be valid"),
65 #[cfg(feature = "json")]
66 UnkeyedJsonValuePolicy::PassThrough,
67 false,
68 )
69});
70/// Lazily initialized fixed strict policy.
71static STRICT_POLICY: LazyLock<RedactionPolicy> = LazyLock::new(|| {
72 RedactionPolicy::from_rules(
73 RedactionRules::new(
74 RedactionPolicyInner {
75 sensitive: Default::default(),
76 allow_exact: Default::default(),
77 allow_suffix: Default::default(),
78 matching: FieldNameMatching::ExactOrTokenSuffix,
79 unknown_field_policy: UnknownFieldPolicy::Redact(Sensitivity::Secret),
80 },
81 Some(RedactionFloor::standard()),
82 ),
83 MaskingPolicy::default(),
84 RedactionLimits::default(),
85 #[cfg(feature = "http")]
86 {
87 let mut http = crate::formats::http::HttpPolicyBuilder::new();
88 http.url_path_mut(crate::formats::http::UrlPathPolicy::Redact);
89 http.text_body_mut(crate::formats::http::TextBodyPolicy::Redact);
90 http.build().expect("the built-in HTTP policy must be valid")
91 },
92 #[cfg(feature = "uri")]
93 crate::formats::uri::UriPolicyBuilder::new()
94 .build()
95 .expect("the built-in URI policy must be valid"),
96 #[cfg(feature = "json")]
97 UnkeyedJsonValuePolicy::Redact,
98 false,
99 )
100});
101/// Immutable field classification, masking, format, and resource policy.
102///
103/// A disabled policy intentionally restores original values while retaining
104/// resource limits. It is a deliberate debugging escape hatch whose
105/// authorization belongs to downstream code.
106///
107/// # Warning
108///
109/// Disabling this policy opts out of confidentiality redaction. Every supported
110/// format, derived field mode, and redaction-specific skip path may publish its
111/// original value. Resource limits and diagnostic control-character escaping
112/// still apply, but they do not make the output redacted. The framework
113/// faithfully executes the chosen policy; it cannot and does not attempt to
114/// prevent downstream code from deliberately or accidentally disabling
115/// redaction. Callers own the authorization, environment, timing, and
116/// consequences of that choice. They can observe it through
117/// [`crate::RedactionSummary::is_redaction_disabled`] and
118/// [`crate::RedactionInspection::is_redaction_disabled`].
119///
120/// # Examples
121///
122/// ```
123/// use qubit_redact::RedactionPolicy;
124///
125/// let mut policy = RedactionPolicy::disabled();
126/// assert!(policy.is_disabled());
127/// policy.set_disabled(false);
128/// assert!(!policy.is_disabled());
129/// ```
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct RedactionPolicy {
132 /// Explicit escape switch that restores source values while retaining
133 /// limits.
134 disabled: bool,
135 /// Immutable field-classification layers.
136 rules: RedactionRules,
137 /// Shared masks selected after sensitivity resolution.
138 masking: Arc<MaskingPolicy>,
139 /// Resource ceilings applied to every transaction created from this policy.
140 limits: RedactionLimits,
141 /// HTTP-specific immutable policy snapshot.
142 #[cfg(feature = "http")]
143 http: Arc<crate::formats::http::HttpPolicy>,
144 /// URI-specific immutable policy snapshot.
145 #[cfg(feature = "uri")]
146 uri: Arc<crate::formats::uri::UriPolicy>,
147 /// Fallback behavior for JSON scalars without an object key.
148 #[cfg(feature = "json")]
149 unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
150}
151
152impl RedactionPolicy {
153 /// Returns the fixed built-in standard policy.
154 ///
155 /// Its application rules are empty and its explicit floor is
156 /// [`RedactionFloor::standard`], so it never observes later process-wide
157 /// default installations.
158 #[must_use]
159 #[inline]
160 pub fn standard() -> Self {
161 STANDARD_POLICY.clone()
162 }
163
164 /// Returns a strict boundary policy whose unknown fields are masked at
165 /// [`Sensitivity::Secret`] in addition to the standard floor.
166 ///
167 /// This preset is intended for untrusted external boundaries. It is more
168 /// protective than [`Self::standard`] but may reduce diagnostic detail.
169 #[must_use]
170 #[inline]
171 pub fn strict() -> Self {
172 STRICT_POLICY.clone()
173 }
174
175 /// Returns the standard policy with confidentiality redaction globally
176 /// disabled.
177 ///
178 /// # Warning
179 ///
180 /// Outputs produced with this policy may contain every original value.
181 /// Limits and control-character escaping remain active, but masking and
182 /// redaction-specific field decisions do not. This deliberate debugging
183 /// capability transfers confidentiality responsibility to the caller.
184 #[must_use]
185 pub fn disabled() -> Self {
186 let mut policy = Self::standard();
187 policy.disabled = true;
188 policy
189 }
190
191 /// Returns whether this policy publishes original values while retaining
192 /// limits and control-character escaping.
193 ///
194 /// A `true` result means confidentiality redaction is disabled.
195 #[must_use]
196 pub const fn is_disabled(&self) -> bool {
197 self.disabled
198 }
199
200 /// Changes the global redaction switch and returns this policy for
201 /// chaining.
202 ///
203 /// # Warning
204 ///
205 /// Passing `true` allows every supported redaction entry to publish its
206 /// original value. The caller owns authorization and operational controls;
207 /// the framework does not distinguish debugging use from misuse.
208 #[must_use]
209 pub fn set_disabled(&mut self, disabled: bool) -> &mut Self {
210 self.disabled = disabled;
211 self
212 }
213
214 /// Creates a deterministic builder with no application rules and the
215 /// standard minimum-protection floor.
216 #[must_use]
217 #[inline]
218 pub fn builder() -> RedactionPolicyBuilder {
219 RedactionPolicyBuilder::new()
220 }
221
222 /// Creates a builder that exactly copies `self`.
223 ///
224 /// The copy includes application rules, limits, and the attached floor.
225 #[must_use]
226 #[inline]
227 pub fn to_builder(&self) -> RedactionPolicyBuilder {
228 RedactionPolicyBuilder::from_policy(self)
229 }
230
231 /// Creates a policy from fully resolved field rules and resource limits.
232 #[must_use]
233 pub(crate) fn from_rules(
234 rules: RedactionRules,
235 masking: MaskingPolicy,
236 limits: RedactionLimits,
237 #[cfg(feature = "http")] http: crate::formats::http::HttpPolicy,
238 #[cfg(feature = "uri")] uri: crate::formats::uri::UriPolicy,
239 #[cfg(feature = "json")] unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
240 disabled: bool,
241 ) -> Self {
242 Self {
243 disabled,
244 rules,
245 masking: Arc::new(masking),
246 limits,
247 #[cfg(feature = "http")]
248 http: Arc::new(http),
249 #[cfg(feature = "uri")]
250 uri: Arc::new(uri),
251 #[cfg(feature = "json")]
252 unkeyed_json_value_policy,
253 }
254 }
255
256 /// Returns all static limits used by this policy.
257 #[must_use]
258 #[inline]
259 pub const fn limits(&self) -> &RedactionLimits {
260 &self.limits
261 }
262
263 /// Returns the unified HTTP context policy.
264 #[must_use]
265 #[cfg(feature = "http")]
266 #[inline]
267 pub fn http(&self) -> &crate::formats::http::HttpPolicy {
268 self.http.as_ref()
269 }
270
271 /// Returns the unified URI context policy.
272 #[must_use]
273 #[cfg(feature = "uri")]
274 #[inline]
275 pub fn uri(&self) -> &crate::formats::uri::UriPolicy {
276 self.uri.as_ref()
277 }
278
279 /// Returns the behavior for root and array JSON scalar values.
280 #[must_use]
281 #[cfg(feature = "json")]
282 #[inline]
283 pub const fn unkeyed_json_value_policy(&self) -> UnkeyedJsonValuePolicy {
284 self.unkeyed_json_value_policy
285 }
286 /// Returns the immutable field rules without diagnostic resource limits.
287 #[must_use]
288 #[inline]
289 pub const fn rules(&self) -> &RedactionRules {
290 &self.rules
291 }
292
293 /// Returns the attached minimum floor, or `None` when it was explicitly
294 /// disabled.
295 #[must_use]
296 #[inline]
297 pub fn floor(&self) -> Option<&RedactionFloor> {
298 self.rules.floor()
299 }
300
301 /// Replaces the floor for this immutable policy.
302 #[must_use]
303 #[inline]
304 pub fn with_floor(mut self, floor: RedactionFloor) -> Self {
305 self.rules = self.rules.with_floor(floor);
306 self
307 }
308 /// Disables every floor for this immutable policy.
309 ///
310 /// # Security
311 ///
312 /// This explicitly removes minimum protection inherited from any source.
313 #[must_use]
314 pub fn disable_floor(mut self) -> Self {
315 self.rules = self.rules.disable_floor();
316 self
317 }
318
319 /// Explains application-rule matching for `field` without applying the
320 /// floor.
321 ///
322 /// This is useful for diagnostics about configured application rules. Use
323 /// [`Self::sensitivity_for`] for the final security decision.
324 #[inline]
325 #[must_use]
326 pub fn classify_field<'a>(&'a self, field: &str) -> FieldClassification<'a> {
327 self.rules.classify_field(field)
328 }
329
330 /// Returns the final sensitivity for `field` after applying application
331 /// rules and the enabled floor.
332 ///
333 /// Returns `None` only when neither layer classifies the field as
334 /// sensitive.
335 #[must_use]
336 #[inline]
337 pub fn sensitivity_for(&self, field: &str) -> Option<Sensitivity> {
338 self.rules.sensitivity_for(field)
339 }
340
341 /// Resolves final sensitivity with exact-only field matching.
342 #[must_use]
343 #[inline]
344 pub(crate) fn sensitivity_for_exact(&self, field: &str) -> Option<Sensitivity> {
345 self.rules.sensitivity_for_exact(field)
346 }
347
348 /// Resolves final sensitivity with exact-only field matching.
349 #[inline]
350 pub(crate) fn resolve_field_exact(&self, field: &str) -> super::ResolvedField {
351 self.rules.resolve_field_exact(field)
352 }
353
354 /// Returns the application layer's field-name matching mode.
355 ///
356 /// An attached floor may use a different matching mode for its independent
357 /// classification.
358 #[must_use]
359 #[inline]
360 pub fn matching(&self) -> FieldNameMatching {
361 self.rules.matching()
362 }
363
364 /// Returns the application layer's fallback for unclassified fields.
365 ///
366 /// An attached floor applies its own fallback independently.
367 #[must_use]
368 #[inline]
369 pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
370 self.rules.unknown_field_policy()
371 }
372 /// Returns the single mask table used by every sensitivity decision.
373 ///
374 /// Field classification determines the effective sensitivity; this table
375 /// determines how that sensitivity is rendered. Floors never own a second
376 /// mask table.
377 #[must_use]
378 #[inline]
379 pub fn masking(&self) -> &MaskingPolicy {
380 self.masking.as_ref()
381 }
382
383 /// Iterates sensitive rules configured in the application layer only.
384 ///
385 /// Use [`Self::floor`] to inspect the independent minimum-protection
386 /// rules.
387 #[inline]
388 pub fn application_sensitive_rules(&self) -> impl Iterator<Item = SensitiveFieldRule<'_>> {
389 self.rules.application_sensitive_rules()
390 }
391
392 /// Iterates allow rules configured in the application layer only.
393 ///
394 /// These rules never bypass an enabled floor.
395 #[inline]
396 pub fn application_allow_rules(&self) -> impl Iterator<Item = AllowRule<'_>> {
397 self.rules.application_allow_rules()
398 }
399
400 /// Resolves final sensitivity for `field`.
401 #[inline]
402 #[must_use]
403 pub(crate) fn resolve_field(&self, field: &str) -> super::ResolvedField {
404 self.rules.resolve_field(field)
405 }
406}
407
408impl Default for RedactionPolicy {
409 /// Clones the fixed standard policy.
410 fn default() -> Self {
411 STANDARD_POLICY.clone()
412 }
413}