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::{
11    Arc,
12    LazyLock,
13    OnceLock,
14};
15
16use super::redaction_limits::RedactionLimits;
17use super::{
18    AllowRule,
19    FieldClassification,
20    FieldNameMatching,
21    MaskingPolicy,
22    RedactionFloor,
23    RedactionPolicyBuilder,
24    RedactionRules,
25    SensitiveFieldRule,
26    Sensitivity,
27    UnknownFieldPolicy,
28    internal::RedactionPolicyInner,
29};
30#[cfg(feature = "json")]
31use super::{
32    JsonDepthBudget,
33    UnkeyedJsonValuePolicy,
34};
35
36/// Built-in sensitive fields not owned by a named preset.
37pub(super) const STANDARD_EXTRA_FIELDS: &[(&str, Sensitivity)] = &[
38    ("auth_app_token", Sensitivity::High),
39    ("auth_user_token", Sensitivity::High),
40    ("connection_string", Sensitivity::Secret),
41    ("database_uri", Sensitivity::Secret),
42    ("database_url", Sensitivity::Secret),
43    ("license_key", Sensitivity::Medium),
44    ("mysql_pwd", Sensitivity::Secret),
45    ("rediscli_auth", Sensitivity::Secret),
46    ("sig", Sensitivity::Secret),
47    ("signature", Sensitivity::Secret),
48];
49
50static STANDARD_POLICY: LazyLock<RedactionPolicy> = LazyLock::new(|| {
51    RedactionPolicy::from_rules(
52        RedactionRules::new(
53            RedactionPolicyInner {
54                sensitive: Default::default(),
55                allow_exact: Default::default(),
56                allow_suffix: Default::default(),
57                matching: FieldNameMatching::ExactOrTokenSuffix,
58                unknown_field_policy: UnknownFieldPolicy::PassThrough,
59            },
60            Some(RedactionFloor::standard()),
61        ),
62        MaskingPolicy::default(),
63        RedactionLimits::default(),
64        #[cfg(feature = "http")]
65        crate::http::HttpPolicyBuilder::new()
66            .build()
67            .expect("the built-in HTTP policy must be valid"),
68        #[cfg(feature = "uri")]
69        crate::uri::UriPolicyBuilder::new()
70            .build()
71            .expect("the built-in URI policy must be valid"),
72        #[cfg(feature = "json")]
73        UnkeyedJsonValuePolicy::PassThrough,
74    )
75});
76static STRICT_POLICY: LazyLock<RedactionPolicy> = LazyLock::new(|| {
77    RedactionPolicy::from_rules(
78        RedactionRules::new(
79            RedactionPolicyInner {
80                sensitive: Default::default(),
81                allow_exact: Default::default(),
82                allow_suffix: Default::default(),
83                matching: FieldNameMatching::ExactOrTokenSuffix,
84                unknown_field_policy: UnknownFieldPolicy::Redact(
85                    Sensitivity::Secret,
86                ),
87            },
88            Some(RedactionFloor::standard()),
89        ),
90        MaskingPolicy::default(),
91        RedactionLimits::default(),
92        #[cfg(feature = "http")]
93        {
94            let mut http = crate::http::HttpPolicyBuilder::new();
95            http.url_path_mut(crate::http::UrlPathPolicy::Redact);
96            http.text_body_mut(crate::http::TextBodyPolicy::Redact);
97            http.build()
98                .expect("the built-in HTTP policy must be valid")
99        },
100        #[cfg(feature = "uri")]
101        crate::uri::UriPolicyBuilder::new()
102            .build()
103            .expect("the built-in URI policy must be valid"),
104        #[cfg(feature = "json")]
105        UnkeyedJsonValuePolicy::Redact,
106    )
107});
108static GLOBAL_POLICY: OnceLock<RedactionPolicy> = OnceLock::new();
109/// Immutable redaction policy.
110#[must_use]
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct RedactionPolicy {
113    rules: RedactionRules,
114    masking: Arc<MaskingPolicy>,
115    limits: RedactionLimits,
116    #[cfg(feature = "http")]
117    http: Arc<crate::http::HttpPolicy>,
118    #[cfg(feature = "uri")]
119    uri: Arc<crate::uri::UriPolicy>,
120    #[cfg(feature = "json")]
121    unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
122}
123
124impl RedactionPolicy {
125    /// Installs the application-owned default policy exactly once.
126    ///
127    /// The policy is copied into a process-wide immutable slot. Reading
128    /// [`Self::global`] before installation only observes [`Self::standard`]
129    /// and does not occupy this slot. If a policy was already installed, the
130    /// rejected policy is returned through
131    /// [`crate::InstallGlobalPolicyError::into_policy`]. Libraries should leave
132    /// this operation to their host application.
133    ///
134    /// # Warning
135    ///
136    /// This is application-assembly configuration, not runtime reconfiguration.
137    /// The executable should call it at most once, after constructing the final
138    /// policy and before starting workers or request processing. Library crates
139    /// must never call it. Calling it from feature code, tests sharing one
140    /// process, or after concurrent application work has begun is a lifecycle
141    /// error even though the type system cannot distinguish those call sites.
142    ///
143    /// Objects created before installation may already own a snapshot of
144    /// [`Self::standard`]. They intentionally keep that snapshot after
145    /// installation. Any object that must use the application policy must be
146    /// created after this call or receive the policy explicitly.
147    pub fn install_global(
148        policy: Self,
149    ) -> Result<(), crate::InstallGlobalPolicyError> {
150        GLOBAL_POLICY
151            .set(policy)
152            .map_err(crate::InstallGlobalPolicyError)
153    }
154
155    /// Returns the process-wide default policy snapshot.
156    ///
157    /// Returns the installed policy when available; otherwise returns the
158    /// fixed standard policy without changing global installation state.
159    /// Existing policy and redactor snapshots are unaffected by a later global
160    /// installation.
161    ///
162    /// # Warning
163    ///
164    /// The pre-installation fallback exists so application assembly may safely
165    /// construct dependencies that consult redaction defaults before the host
166    /// has finalized its policy. It is not a runtime configuration mechanism.
167    /// A caller that requires the application policy must either run after
168    /// [`Self::install_global`] or use an explicitly injected policy. Never
169    /// assume that a value returned before installation will change afterward.
170    #[inline]
171    pub fn global() -> &'static Self {
172        GLOBAL_POLICY.get().unwrap_or(&STANDARD_POLICY)
173    }
174
175    /// Returns the fixed built-in standard policy.
176    ///
177    /// Its application rules are empty and its explicit floor is
178    /// [`RedactionFloor::standard`], so it never observes later process-wide
179    /// default installations.
180    #[inline]
181    pub fn standard() -> Self {
182        STANDARD_POLICY.clone()
183    }
184
185    /// Returns a strict boundary policy whose unknown fields are masked at
186    /// [`Sensitivity::Secret`] in addition to the standard floor.
187    ///
188    /// This preset is intended for untrusted external boundaries. It is more
189    /// protective than [`Self::standard`] but may reduce diagnostic detail.
190    #[inline]
191    pub fn strict() -> Self {
192        STRICT_POLICY.clone()
193    }
194
195    /// Creates a builder initialized from the process-wide default snapshot.
196    #[inline]
197    pub fn builder_from_default() -> RedactionPolicyBuilder {
198        Self::builder_from(&Self::default())
199    }
200
201    /// Creates a deterministic builder with no application rules and the
202    /// standard minimum-protection floor.
203    #[inline]
204    pub fn builder() -> RedactionPolicyBuilder {
205        RedactionPolicyBuilder::new()
206    }
207
208    /// Creates a builder that exactly copies `self`.
209    ///
210    /// The copy includes application rules, limits, and the attached floor.
211    #[inline]
212    pub fn to_builder(&self) -> RedactionPolicyBuilder {
213        RedactionPolicyBuilder::from_policy(self)
214    }
215
216    /// Creates a builder that exactly copies `base`.
217    #[inline]
218    pub fn builder_from(base: &Self) -> RedactionPolicyBuilder {
219        base.to_builder()
220    }
221
222    /// Creates a policy from fully resolved field rules and resource limits.
223    pub(crate) fn from_rules(
224        rules: RedactionRules,
225        masking: MaskingPolicy,
226        limits: RedactionLimits,
227        #[cfg(feature = "http")] http: crate::http::HttpPolicy,
228        #[cfg(feature = "uri")] uri: crate::uri::UriPolicy,
229        #[cfg(feature = "json")]
230        unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
231    ) -> Self {
232        Self {
233            rules,
234            masking: Arc::new(masking),
235            limits,
236            #[cfg(feature = "http")]
237            http: Arc::new(http),
238            #[cfg(feature = "uri")]
239            uri: Arc::new(uri),
240            #[cfg(feature = "json")]
241            unkeyed_json_value_policy,
242        }
243    }
244
245    /// Returns all static limits used by this policy.
246    #[inline]
247    pub const fn limits(&self) -> &RedactionLimits {
248        &self.limits
249    }
250
251    /// Returns the unified HTTP context policy.
252    #[cfg(feature = "http")]
253    #[inline]
254    pub fn http(&self) -> &crate::http::HttpPolicy {
255        self.http.as_ref()
256    }
257
258    /// Returns the unified URI context policy.
259    #[cfg(feature = "uri")]
260    #[inline]
261    pub fn uri(&self) -> &crate::uri::UriPolicy {
262        self.uri.as_ref()
263    }
264
265    /// Returns the HTTP header field rules.
266    #[cfg(feature = "http")]
267    #[inline]
268    pub fn header_rules(&self) -> &RedactionRules {
269        self.http.header_rules()
270    }
271
272    /// Returns the HTTP query field rules.
273    #[cfg(feature = "http")]
274    #[inline]
275    pub fn query_rules(&self) -> &RedactionRules {
276        self.http.query_rules()
277    }
278
279    /// Returns the HTTP body field rules.
280    #[cfg(feature = "http")]
281    #[inline]
282    pub fn body_rules(&self) -> &RedactionRules {
283        self.http.body_rules()
284    }
285
286    /// Returns the HTTP URL path policy.
287    #[cfg(feature = "http")]
288    #[inline]
289    pub fn url_path_policy(&self) -> crate::http::UrlPathPolicy {
290        self.http.url_path_policy()
291    }
292
293    /// Returns the HTTP text-body policy.
294    #[cfg(feature = "http")]
295    #[inline]
296    pub fn text_body_policy(&self) -> crate::http::TextBodyPolicy {
297        self.http.text_body_policy()
298    }
299
300    /// Returns the HTTP body byte budget.
301    #[cfg(feature = "http")]
302    #[inline]
303    pub fn body_budget(&self) -> crate::http::BodyBudget {
304        self.limits.http_body()
305    }
306
307    /// Returns the URI path policy.
308    #[cfg(feature = "uri")]
309    #[inline]
310    pub fn path_policy(&self) -> crate::uri::UriPathPolicy {
311        self.uri.path_policy()
312    }
313
314    /// Returns the URI fragment policy.
315    #[cfg(feature = "uri")]
316    #[inline]
317    pub fn fragment_policy(&self) -> crate::uri::UriFragmentPolicy {
318        self.uri.fragment_policy()
319    }
320
321    /// Returns the maximum JSON nesting depth for JSON redaction.
322    #[cfg(feature = "json")]
323    #[inline]
324    pub const fn json_depth_budget(&self) -> JsonDepthBudget {
325        self.limits.json_depth_budget()
326    }
327
328    /// Returns the behavior for root and array JSON scalar values.
329    #[cfg(feature = "json")]
330    #[inline]
331    pub const fn unkeyed_json_value_policy(&self) -> UnkeyedJsonValuePolicy {
332        self.unkeyed_json_value_policy
333    }
334    /// Returns the immutable field rules without diagnostic resource limits.
335    #[inline]
336    pub const fn rules(&self) -> &RedactionRules {
337        &self.rules
338    }
339
340    /// Returns the base field policy view.
341    #[inline]
342    pub const fn fields(&self) -> &RedactionRules {
343        &self.rules
344    }
345
346    /// Returns the attached minimum floor, or `None` when it was explicitly
347    /// disabled.
348    #[inline]
349    pub fn floor(&self) -> Option<&RedactionFloor> {
350        self.rules.floor()
351    }
352
353    /// Replaces the floor for this immutable policy.
354    pub fn with_floor(mut self, floor: RedactionFloor) -> Self {
355        self.rules = self.rules.with_floor(floor);
356        self
357    }
358    /// Disables every floor for this immutable policy.
359    ///
360    /// # Security
361    ///
362    /// This explicitly removes minimum protection inherited from any source.
363    pub fn disable_floor(mut self) -> Self {
364        self.rules = self.rules.disable_floor();
365        self
366    }
367
368    /// Explains application-rule matching for `field` without applying the
369    /// floor.
370    ///
371    /// This is useful for diagnostics about configured application rules. Use
372    /// [`Self::sensitivity_for`] for the final security decision.
373    #[inline]
374    pub fn classify_field<'a>(
375        &'a self,
376        field: &str,
377    ) -> FieldClassification<'a> {
378        self.rules.classify_field(field)
379    }
380
381    /// Returns the final sensitivity for `field` after applying application
382    /// rules and the enabled floor.
383    ///
384    /// Returns `None` only when neither layer classifies the field as
385    /// sensitive.
386    #[inline]
387    pub fn sensitivity_for(&self, field: &str) -> Option<Sensitivity> {
388        self.rules.sensitivity_for(field)
389    }
390
391    /// Resolves final sensitivity with exact-only field matching.
392    #[inline]
393    pub(crate) fn sensitivity_for_exact(
394        &self,
395        field: &str,
396    ) -> Option<Sensitivity> {
397        self.rules.sensitivity_for_exact(field)
398    }
399
400    /// Resolves final sensitivity with exact-only field matching.
401    #[inline]
402    pub(crate) fn resolve_field_exact(
403        &self,
404        field: &str,
405    ) -> super::ResolvedField {
406        self.rules.resolve_field_exact(field)
407    }
408
409    /// Returns the application layer's field-name matching mode.
410    ///
411    /// An attached floor may use a different matching mode for its independent
412    /// classification.
413    #[inline]
414    pub fn matching(&self) -> FieldNameMatching {
415        self.rules.matching()
416    }
417
418    /// Returns the application layer's fallback for unclassified fields.
419    ///
420    /// An attached floor applies its own fallback independently.
421    #[inline]
422    pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
423        self.rules.unknown_field_policy()
424    }
425    /// Returns the single mask table used by every sensitivity decision.
426    ///
427    /// Field classification determines the effective sensitivity; this table
428    /// determines how that sensitivity is rendered. Floors never own a second
429    /// mask table.
430    #[inline]
431    pub fn masking(&self) -> &MaskingPolicy {
432        self.masking.as_ref()
433    }
434
435    /// Iterates sensitive rules configured in the application layer only.
436    ///
437    /// Use [`Self::floor`] to inspect the independent minimum-protection
438    /// rules.
439    #[inline]
440    pub fn application_sensitive_rules(
441        &self,
442    ) -> impl Iterator<Item = SensitiveFieldRule<'_>> {
443        self.rules.application_sensitive_rules()
444    }
445
446    /// Iterates allow rules configured in the application layer only.
447    ///
448    /// These rules never bypass an enabled floor.
449    #[inline]
450    pub fn application_allow_rules(
451        &self,
452    ) -> impl Iterator<Item = AllowRule<'_>> {
453        self.rules.application_allow_rules()
454    }
455
456    /// Resolves final sensitivity for `field`.
457    #[inline]
458    pub(crate) fn resolve_field(&self, field: &str) -> super::ResolvedField {
459        self.rules.resolve_field(field)
460    }
461}
462
463impl Default for RedactionPolicy {
464    /// Clones the currently visible process default.
465    ///
466    /// # Warning
467    ///
468    /// Before application assembly calls [`Self::install_global`], this clones
469    /// [`Self::standard`]. The clone is a permanent snapshot and will not be
470    /// updated by a later installation. Policy-sensitive objects that require
471    /// application configuration must be constructed after installation or be
472    /// given an explicit policy.
473    fn default() -> Self {
474        Self::global().clone()
475    }
476}