Skip to main content

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}