Skip to main content

qubit_redact/policy/field/
redaction_floor.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//! Minimum field-protection floors.
9
10use std::fmt;
11use std::sync::Arc;
12use std::sync::LazyLock;
13
14use super::FieldNameMatching;
15use super::RedactionFloorBuilder;
16use super::SensitiveFieldPreset;
17use super::SensitiveFieldRule;
18use super::UnknownFieldPolicy;
19use crate::policy::internal::RedactionPolicyInner;
20
21/// Immutable minimum field-protection rules.
22///
23/// A floor contains sensitive-field rules, matching behavior, and an
24/// unknown-field fallback. It intentionally has no allow rules or mask table.
25///
26/// # Examples
27///
28/// ```
29/// use qubit_redact::RedactionFloor;
30/// use qubit_redact::RedactionPolicy;
31/// use qubit_redact::Sensitivity;
32///
33/// let floor = RedactionFloor::builder().raise("pin", Sensitivity::Secret)?.build()?;
34/// let policy = RedactionPolicy::standard().with_floor(floor);
35/// assert_eq!(policy.sensitivity_for("pin"), Some(Sensitivity::Secret));
36/// # Ok::<(), qubit_redact::PolicyError>(())
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RedactionFloor {
40    /// Shared immutable rule state containing only minimum protections.
41    pub(crate) inner: Arc<RedactionPolicyInner>,
42}
43
44/// Lazily initialized conservative floor shared by standard policies.
45static STANDARD_FLOOR: LazyLock<RedactionFloor> = LazyLock::new(|| {
46    let mut builder = RedactionFloor::builder();
47    for preset in [
48        SensitiveFieldPreset::Credentials,
49        SensitiveFieldPreset::CredentialContainers,
50        SensitiveFieldPreset::AuthTokens,
51        SensitiveFieldPreset::Http,
52        SensitiveFieldPreset::Session,
53    ] {
54        builder = builder.include_preset(preset);
55    }
56    for &(field, level) in super::super::redaction_policy::STANDARD_EXTRA_FIELDS {
57        builder = builder
58            .raise(field, level)
59            .expect("built-in standard floor fields must be valid");
60    }
61    builder.build().expect("the built-in redaction floor is valid")
62});
63
64impl RedactionFloor {
65    /// Returns the built-in conservative floor.
66    #[must_use]
67    #[inline(always)]
68    pub fn standard() -> Self {
69        STANDARD_FLOOR.clone()
70    }
71
72    /// Creates a deterministic empty floor builder.
73    #[must_use]
74    #[inline(always)]
75    pub fn builder() -> RedactionFloorBuilder {
76        RedactionFloorBuilder::empty()
77    }
78
79    /// Iterates the floor's canonical sensitive rules.
80    pub fn sensitive_rules(&self) -> impl Iterator<Item = SensitiveFieldRule<'_>> {
81        self.inner
82            .sensitive
83            .iter()
84            .map(|(field, level)| SensitiveFieldRule::new(field, *level))
85    }
86
87    /// Creates a floor builder by copying `self` exactly.
88    #[must_use]
89    #[inline(always)]
90    pub fn to_builder(&self) -> RedactionFloorBuilder {
91        RedactionFloorBuilder::from_floor(self)
92    }
93
94    /// Combines two floors by retaining the strongest classification for every
95    /// canonical field. This is used when a format boundary adds mandatory
96    /// protection to an application policy.
97    #[must_use]
98    pub(crate) fn combine(&self, other: &Self) -> Self {
99        let mut sensitive = self.inner.sensitive.clone();
100        for (field, level) in &other.inner.sensitive {
101            sensitive
102                .entry(field.clone())
103                .and_modify(|current| *current = (*current).max(*level))
104                .or_insert(*level);
105        }
106        let unknown = match (
107            self.inner.unknown_field_policy.sensitivity(),
108            other.inner.unknown_field_policy.sensitivity(),
109        ) {
110            (Some(left), Some(right)) => Some(left.max(right)),
111            (Some(level), None) | (None, Some(level)) => Some(level),
112            (None, None) => None,
113        };
114        let unknown_field_policy = unknown.map_or(UnknownFieldPolicy::PassThrough, UnknownFieldPolicy::Redact);
115        Self {
116            inner: std::sync::Arc::new(RedactionPolicyInner {
117                sensitive,
118                allow_exact: Default::default(),
119                allow_suffix: Default::default(),
120                matching: if self.inner.matching == FieldNameMatching::ExactOrTokenSuffix
121                    || other.inner.matching == FieldNameMatching::ExactOrTokenSuffix
122                {
123                    FieldNameMatching::ExactOrTokenSuffix
124                } else {
125                    FieldNameMatching::Exact
126                },
127                unknown_field_policy,
128            }),
129        }
130    }
131}
132
133impl Default for RedactionFloor {
134    /// Returns the built-in conservative floor.
135    #[inline(always)]
136    fn default() -> Self {
137        Self::standard()
138    }
139}
140
141impl fmt::Display for RedactionFloor {
142    /// Writes the diagnostic type name, propagating any destination formatter
143    /// error.
144    #[inline(always)]
145    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146        formatter.write_str("RedactionFloor")
147    }
148}