Skip to main content

qubit_redact/policy/
redaction_policy_builder.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//! Mutable builder for immutable redaction policies.
9
10use super::{
11    FieldNameMatching,
12    InputOutputLimit,
13    MaskPolicy,
14    MaskingPolicy,
15    PolicyError,
16    PolicyLocation,
17    RedactionFloor,
18    RedactionLimits,
19    RedactionPolicy,
20    RedactionRules,
21    RedactionRulesBuilder,
22    SensitiveFieldPreset,
23    Sensitivity,
24    UnknownFieldPolicy,
25};
26#[cfg(feature = "json")]
27use super::{
28    JsonDepthBudget,
29    UnkeyedJsonValuePolicy,
30};
31
32/// Mutable construction state for an immutable [`RedactionPolicy`].
33#[must_use]
34#[derive(Debug, Clone)]
35pub struct RedactionPolicyBuilder {
36    rules: RedactionRulesBuilder,
37    masking: MaskingPolicy,
38    floor: Option<RedactionFloor>,
39    limits: RedactionLimits,
40    #[cfg(feature = "http")]
41    http: crate::http::HttpPolicyBuilder,
42    #[cfg(feature = "uri")]
43    uri: crate::uri::UriPolicyBuilder,
44    #[cfg(feature = "json")]
45    unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
46}
47
48impl RedactionPolicyBuilder {
49    /// Creates an empty application-rule builder with the standard floor.
50    pub fn new() -> Self {
51        Self {
52            rules: RedactionRulesBuilder::empty(PolicyLocation::Rules),
53            masking: MaskingPolicy::default(),
54            floor: Some(RedactionFloor::standard()),
55            limits: RedactionLimits::default(),
56            #[cfg(feature = "http")]
57            http: crate::http::HttpPolicyBuilder::new(),
58            #[cfg(feature = "uri")]
59            uri: crate::uri::UriPolicyBuilder::new(),
60            #[cfg(feature = "json")]
61            unkeyed_json_value_policy: UnkeyedJsonValuePolicy::PassThrough,
62        }
63    }
64    pub(super) fn from_policy(policy: &RedactionPolicy) -> Self {
65        Self {
66            rules: RedactionRulesBuilder::from_inner(
67                &policy.rules().clone_application(),
68                PolicyLocation::Rules,
69            ),
70            masking: policy.masking().clone(),
71            floor: policy.rules().floor().cloned(),
72            limits: *policy.limits(),
73            #[cfg(feature = "http")]
74            http: crate::http::HttpPolicyBuilder::from_policy(policy.http()),
75            #[cfg(feature = "uri")]
76            uri: crate::uri::UriPolicyBuilder::from_policy(policy.uri()),
77            #[cfg(feature = "json")]
78            unkeyed_json_value_policy: policy.unkeyed_json_value_policy(),
79        }
80    }
81
82    /// Returns the mutable base-field configuration view.
83    pub fn fields(&mut self) -> FieldsBuilder<'_> {
84        FieldsBuilder { builder: self }
85    }
86
87    /// Returns the mutable HTTP configuration view.
88    #[cfg(feature = "http")]
89    pub fn http(&mut self) -> HttpPolicyBuilderView<'_> {
90        HttpPolicyBuilderView { builder: self }
91    }
92
93    /// Returns the mutable URI configuration view.
94    #[cfg(feature = "uri")]
95    pub fn uri(&mut self) -> UriPolicyBuilderView<'_> {
96        UriPolicyBuilderView {
97            builder: &mut self.uri,
98        }
99    }
100
101    /// Returns the mutable static-limits configuration view.
102    pub fn limits(&mut self) -> LimitsBuilder<'_> {
103        LimitsBuilder { builder: self }
104    }
105
106    /// Validates that `field` has a non-empty canonical application-rule name.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`PolicyError::EmptyFieldName`] at
111    /// [`PolicyLocation::Rules`] when canonicalization leaves no name.
112    pub fn validate_field_name(field: &str) -> Result<(), PolicyError> {
113        RedactionRulesBuilder::validate_field_name(field, PolicyLocation::Rules)
114    }
115
116    /// Replaces the floor snapshot and marks it as explicitly configured.
117    ///
118    /// This is last-call-wins with [`Self::disable_floor`].
119    pub fn floor(mut self, floor: RedactionFloor) -> Self {
120        self.floor = Some(floor);
121        self
122    }
123    /// Disables every floor, including the standard floor.
124    ///
125    /// # Security
126    ///
127    /// This removes minimum field protection. Call it only when this is an
128    /// intentional, reviewed decision by the policy owner.
129    pub fn disable_floor(mut self) -> Self {
130        self.floor = None;
131        self
132    }
133
134    /// Sets application field-name matching behavior.
135    pub fn matching(mut self, matching: FieldNameMatching) -> Self {
136        self.rules.matching(matching);
137        self
138    }
139
140    /// Sets the application fallback for unclassified fields.
141    pub fn unknown_field_policy(mut self, policy: UnknownFieldPolicy) -> Self {
142        self.rules.unknown_field_policy(policy);
143        self
144    }
145
146    /// Adds every sensitive field defined by `preset` to application rules.
147    pub fn include_preset(mut self, preset: SensitiveFieldPreset) -> Self {
148        self.rules.include_preset(preset);
149        self
150    }
151
152    /// Raises application sensitivity for `field` to at least `level`.
153    ///
154    /// Repeated calls for the same canonical field are monotonic. When several
155    /// differently named rules match one input field, final resolution uses
156    /// the strongest matching sensitivity. For example, `token = Secret` and
157    /// `access_token = Medium` resolve `OPENAI_ACCESS_TOKEN` to `Secret`.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`PolicyError::EmptyFieldName`] when `field` has no canonical
162    /// application-rule name.
163    pub fn raise(
164        mut self,
165        field: &str,
166        level: Sensitivity,
167    ) -> Result<Self, PolicyError> {
168        self.rules.raise(field, level)?;
169        Ok(self)
170    }
171
172    /// Replaces the application sensitivity for `field` with `level`.
173    ///
174    /// This replaces only the same canonical rule. It does not weaken an
175    /// enabled floor or a stronger overlapping rule with another name.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`PolicyError::EmptyFieldName`] when `field` has no canonical
180    /// application-rule name.
181    pub fn override_level(
182        mut self,
183        field: &str,
184        level: Sensitivity,
185    ) -> Result<Self, PolicyError> {
186        self.rules.override_level(field, level)?;
187        Ok(self)
188    }
189
190    /// Allows one canonical exact application field name.
191    ///
192    /// An enabled floor remains independently effective for the same field.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`PolicyError::EmptyFieldName`] when `field` has no canonical
197    /// application-rule name.
198    pub fn allow_canonical_exact(
199        mut self,
200        field: &str,
201    ) -> Result<Self, PolicyError> {
202        self.rules.allow_canonical_exact(field)?;
203        Ok(self)
204    }
205
206    /// Allows one application field-name token suffix.
207    ///
208    /// An enabled floor remains independently effective for matching fields.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`PolicyError::EmptyFieldName`] when `field` has no canonical
213    /// application-rule name.
214    pub fn allow_suffix(mut self, field: &str) -> Result<Self, PolicyError> {
215        self.rules.allow_suffix(field)?;
216        Ok(self)
217    }
218
219    /// Removes the exact application allow rule for `field` when present.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`PolicyError::EmptyFieldName`] when `field` has no canonical
224    /// application-rule name.
225    pub fn remove_allow_canonical_exact(
226        mut self,
227        field: &str,
228    ) -> Result<Self, PolicyError> {
229        self.rules.remove_allow_canonical_exact(field)?;
230        Ok(self)
231    }
232
233    /// Removes the suffix application allow rule for `field` when present.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`PolicyError::EmptyFieldName`] when `field` has no canonical
238    /// application-rule name.
239    pub fn remove_allow_suffix(
240        mut self,
241        field: &str,
242    ) -> Result<Self, PolicyError> {
243        self.rules.remove_allow_suffix(field)?;
244        Ok(self)
245    }
246
247    /// Removes every application allow rule.
248    pub fn clear_allow_rules(mut self) -> Self {
249        self.rules.clear_allow_rules();
250        self
251    }
252
253    /// Sets the application masking policy for values at `level`.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`PolicyError::EmptyFixedReplacement`] when `policy` supplies
258    /// an empty fixed replacement.
259    pub fn mask(
260        mut self,
261        level: Sensitivity,
262        policy: MaskPolicy,
263    ) -> Result<Self, PolicyError> {
264        let masking = self.masking.with_policy(level, policy);
265        masking.validate(PolicyLocation::Rules)?;
266        self.masking = masking;
267        Ok(self)
268    }
269
270    /// Sets the diagnostic-event input and output limits.
271    pub const fn diagnostic_event(mut self, budget: InputOutputLimit) -> Self {
272        self.limits = self.limits.with_diagnostic_event(budget);
273        self
274    }
275
276    /// Sets the ordinary operation input and output limits.
277    pub const fn ordinary_operation(
278        mut self,
279        budget: InputOutputLimit,
280    ) -> Self {
281        self.limits = self.limits.with_ordinary_operation(budget);
282        self
283    }
284
285    /// Sets HTTP URL path handling in the unified policy.
286    #[cfg(feature = "http")]
287    pub fn url_path_policy(
288        mut self,
289        policy: crate::http::UrlPathPolicy,
290    ) -> Self {
291        self.http.url_path_mut(policy);
292        self
293    }
294
295    /// Sets HTTP opaque text-body handling in the unified policy.
296    #[cfg(feature = "http")]
297    pub fn text_body_policy(
298        mut self,
299        policy: crate::http::TextBodyPolicy,
300    ) -> Self {
301        self.http.text_body_mut(policy);
302        self
303    }
304
305    /// Sets HTTP structured body limits in the unified policy.
306    #[cfg(feature = "http")]
307    pub fn body_budget(mut self, budget: crate::http::BodyBudget) -> Self {
308        self.limits = self.limits.with_http_body(budget);
309        self
310    }
311
312    /// Sets URI path handling in the unified policy.
313    #[cfg(feature = "uri")]
314    pub fn path_policy(mut self, policy: crate::uri::UriPathPolicy) -> Self {
315        self.uri = self.uri.path_policy(policy);
316        self
317    }
318
319    /// Sets URI fragment handling in the unified policy.
320    #[cfg(feature = "uri")]
321    pub fn fragment_policy(
322        mut self,
323        policy: crate::uri::UriFragmentPolicy,
324    ) -> Self {
325        self.uri = self.uri.fragment_policy(policy);
326        self
327    }
328
329    /// Sets the maximum JSON nesting depth used by JSON redaction.
330    #[cfg(feature = "json")]
331    pub const fn json_depth_budget(mut self, budget: JsonDepthBudget) -> Self {
332        self.limits = self.limits.with_json_depth_budget(budget);
333        self
334    }
335
336    /// Sets behavior for root and array JSON scalar values.
337    #[cfg(feature = "json")]
338    pub const fn unkeyed_json_value_policy(
339        mut self,
340        policy: UnkeyedJsonValuePolicy,
341    ) -> Self {
342        self.unkeyed_json_value_policy = policy;
343        self
344    }
345
346    /// Validates and returns the immutable policy snapshot.
347    ///
348    /// # Errors
349    ///
350    /// Returns a [`PolicyError`] when final policy validation fails.
351    pub fn build(self) -> Result<RedactionPolicy, PolicyError> {
352        let rules = RedactionRules::new(self.rules.build_inner()?, self.floor);
353        self.masking.validate(PolicyLocation::Rules)?;
354        #[cfg(feature = "http")]
355        let http = self.http.build()?;
356        #[cfg(feature = "uri")]
357        let uri = self.uri.build()?;
358        Ok(RedactionPolicy::from_rules(
359            rules,
360            self.masking,
361            self.limits,
362            #[cfg(feature = "http")]
363            http,
364            #[cfg(feature = "uri")]
365            uri,
366            #[cfg(feature = "json")]
367            self.unkeyed_json_value_policy,
368        ))
369    }
370}
371
372mod views {
373    #[cfg(feature = "json")]
374    use super::JsonDepthBudget;
375    #[cfg(feature = "http")]
376    use super::RedactionRules;
377    use super::{
378        FieldNameMatching,
379        InputOutputLimit,
380        MaskPolicy,
381        PolicyError,
382        PolicyLocation,
383        RedactionFloor,
384        RedactionPolicyBuilder,
385        SensitiveFieldPreset,
386        Sensitivity,
387        UnknownFieldPolicy,
388    };
389
390    /// Mutable view over the base field policy.
391    #[must_use]
392    pub struct FieldsBuilder<'a> {
393        pub(super) builder: &'a mut RedactionPolicyBuilder,
394    }
395
396    impl FieldsBuilder<'_> {
397        /// Sets field-name matching for the base policy.
398        pub fn matching(&mut self, matching: FieldNameMatching) -> &mut Self {
399            self.builder.rules.matching(matching);
400            self
401        }
402
403        /// Sets the base fallback for unknown fields.
404        pub fn unknown_field_policy(
405            &mut self,
406            policy: UnknownFieldPolicy,
407        ) -> &mut Self {
408            self.builder.rules.unknown_field_policy(policy);
409            self
410        }
411
412        /// Includes all fields from a built-in sensitive preset.
413        pub fn include_preset(
414            &mut self,
415            preset: SensitiveFieldPreset,
416        ) -> &mut Self {
417            self.builder.rules.include_preset(preset);
418            self
419        }
420
421        /// Raises a base field's minimum sensitivity.
422        pub fn raise(
423            &mut self,
424            field: &str,
425            level: Sensitivity,
426        ) -> Result<&mut Self, PolicyError> {
427            self.builder.rules.raise(field, level)?;
428            Ok(self)
429        }
430
431        /// Replaces one base field rule without weakening floors.
432        pub fn override_level(
433            &mut self,
434            field: &str,
435            level: Sensitivity,
436        ) -> Result<&mut Self, PolicyError> {
437            self.builder.rules.override_level(field, level)?;
438            Ok(self)
439        }
440
441        /// Adds a base exact allow rule.
442        pub fn allow_exact(
443            &mut self,
444            field: &str,
445        ) -> Result<&mut Self, PolicyError> {
446            self.builder.rules.allow_canonical_exact(field)?;
447            Ok(self)
448        }
449
450        /// Adds a base suffix allow rule.
451        pub fn allow_suffix(
452            &mut self,
453            field: &str,
454        ) -> Result<&mut Self, PolicyError> {
455            self.builder.rules.allow_suffix(field)?;
456            Ok(self)
457        }
458
459        /// Removes a base exact allow rule.
460        pub fn remove_allow_exact(
461            &mut self,
462            field: &str,
463        ) -> Result<&mut Self, PolicyError> {
464            self.builder.rules.remove_allow_canonical_exact(field)?;
465            Ok(self)
466        }
467
468        /// Removes a base suffix allow rule.
469        pub fn remove_allow_suffix(
470            &mut self,
471            field: &str,
472        ) -> Result<&mut Self, PolicyError> {
473            self.builder.rules.remove_allow_suffix(field)?;
474            Ok(self)
475        }
476
477        /// Removes all base allow rules.
478        pub fn clear_allow_rules(&mut self) -> &mut Self {
479            self.builder.rules.clear_allow_rules();
480            self
481        }
482
483        /// Replaces the base minimum-protection floor.
484        pub fn floor(&mut self, floor: RedactionFloor) -> &mut Self {
485            self.builder.floor = Some(floor);
486            self
487        }
488
489        /// Disables the base floor explicitly.
490        pub fn disable_floor(&mut self) -> &mut Self {
491            self.builder.floor = None;
492            self
493        }
494
495        /// Replaces one shared masking level.
496        pub fn mask(
497            &mut self,
498            level: Sensitivity,
499            policy: MaskPolicy,
500        ) -> Result<&mut Self, PolicyError> {
501            let masking =
502                self.builder.masking.clone().with_policy(level, policy);
503            masking.validate(PolicyLocation::Rules)?;
504            self.builder.masking = masking;
505            Ok(self)
506        }
507    }
508
509    /// Mutable view over all HTTP context differences.
510    #[cfg(feature = "http")]
511    #[must_use]
512    pub struct HttpPolicyBuilderView<'a> {
513        pub(super) builder: &'a mut RedactionPolicyBuilder,
514    }
515
516    /// Mutable view over URI-specific behavior.
517    #[cfg(feature = "uri")]
518    #[must_use]
519    pub struct UriPolicyBuilderView<'a> {
520        pub(super) builder: &'a mut crate::uri::UriPolicyBuilder,
521    }
522
523    #[cfg(feature = "uri")]
524    impl UriPolicyBuilderView<'_> {
525        /// Sets URI path visibility.
526        pub fn path(&mut self, policy: crate::uri::UriPathPolicy) -> &mut Self {
527            self.builder.path_policy_mut(policy);
528            self
529        }
530
531        /// Sets URI fragment visibility.
532        pub fn fragment(
533            &mut self,
534            policy: crate::uri::UriFragmentPolicy,
535        ) -> &mut Self {
536            self.builder.fragment_policy_mut(policy);
537            self
538        }
539    }
540
541    #[cfg(feature = "http")]
542    impl HttpPolicyBuilderView<'_> {
543        /// Returns the header context view.
544        pub fn header(&mut self) -> HttpContextBuilderView<'_> {
545            HttpContextBuilderView {
546                builder: &mut self.builder.http,
547                context: crate::http::HttpFieldContext::Header,
548            }
549        }
550
551        /// Returns the query/form context view.
552        pub fn query(&mut self) -> HttpContextBuilderView<'_> {
553            HttpContextBuilderView {
554                builder: &mut self.builder.http,
555                context: crate::http::HttpFieldContext::Query,
556            }
557        }
558
559        /// Returns the structured-body context view.
560        pub fn body(&mut self) -> HttpContextBuilderView<'_> {
561            HttpContextBuilderView {
562                builder: &mut self.builder.http,
563                context: crate::http::HttpFieldContext::Body,
564            }
565        }
566
567        /// Sets URL path visibility for HTTP diagnostics.
568        pub fn url_path(
569            &mut self,
570            policy: crate::http::UrlPathPolicy,
571        ) -> &mut Self {
572            self.builder.http.url_path_mut(policy);
573            self
574        }
575
576        /// Sets opaque text-body visibility for HTTP diagnostics.
577        pub fn text_body(
578            &mut self,
579            policy: crate::http::TextBodyPolicy,
580        ) -> &mut Self {
581            self.builder.http.text_body_mut(policy);
582            self
583        }
584
585        /// Sets the same floor for every HTTP field context.
586        pub fn floor_all(&mut self, floor: RedactionFloor) -> &mut Self {
587            self.builder.http.floor_all_mut(floor);
588            self
589        }
590
591        /// Disables every HTTP field-context floor explicitly.
592        pub fn disable_all_floors(&mut self) -> &mut Self {
593            self.builder.http.disable_all_floors_mut();
594            self
595        }
596
597        /// Sets the handling of root and array JSON scalar values in HTTP
598        /// bodies.
599        pub fn unkeyed_json(
600            &mut self,
601            policy: crate::http::UnkeyedJsonValuePolicy,
602        ) -> &mut Self {
603            self.builder.unkeyed_json_value_policy = policy;
604            self
605        }
606    }
607
608    /// Mutable view over one HTTP field context.
609    #[cfg(feature = "http")]
610    #[must_use]
611    pub struct HttpContextBuilderView<'a> {
612        builder: &'a mut crate::http::HttpPolicyBuilder,
613        context: crate::http::HttpFieldContext,
614    }
615
616    #[cfg(feature = "http")]
617    impl HttpContextBuilderView<'_> {
618        /// Replaces all rules for this HTTP field context.
619        pub fn replace_rules(&mut self, rules: RedactionRules) -> &mut Self {
620            self.builder.rules_mut(self.context, rules);
621            self
622        }
623
624        /// Raises a context field's minimum sensitivity.
625        pub fn raise(
626            &mut self,
627            field: &str,
628            level: Sensitivity,
629        ) -> Result<&mut Self, PolicyError> {
630            self.builder.raise_mut(self.context, field, level)?;
631            Ok(self)
632        }
633
634        /// Replaces a context field rule without weakening the base policy.
635        pub fn override_level(
636            &mut self,
637            field: &str,
638            level: Sensitivity,
639        ) -> Result<&mut Self, PolicyError> {
640            self.builder
641                .override_level_mut(self.context, field, level)?;
642            Ok(self)
643        }
644
645        /// Adds a context exact allow rule; the base policy still applies.
646        pub fn allow_exact(
647            &mut self,
648            field: &str,
649        ) -> Result<&mut Self, PolicyError> {
650            self.builder.allow_exact_mut(self.context, field)?;
651            Ok(self)
652        }
653
654        /// Adds a context suffix allow rule; the base policy still applies.
655        pub fn allow_suffix(
656            &mut self,
657            field: &str,
658        ) -> Result<&mut Self, PolicyError> {
659            self.builder.allow_suffix_mut(self.context, field)?;
660            Ok(self)
661        }
662
663        /// Removes a context exact allow rule.
664        pub fn remove_allow_exact(
665            &mut self,
666            field: &str,
667        ) -> Result<&mut Self, PolicyError> {
668            self.builder.remove_allow_exact_mut(self.context, field)?;
669            Ok(self)
670        }
671
672        /// Removes a context suffix allow rule.
673        pub fn remove_allow_suffix(
674            &mut self,
675            field: &str,
676        ) -> Result<&mut Self, PolicyError> {
677            self.builder.remove_allow_suffix_mut(self.context, field)?;
678            Ok(self)
679        }
680
681        /// Removes all context allow rules.
682        pub fn clear_allow_rules(&mut self) -> &mut Self {
683            self.builder.clear_allow_rules_mut(self.context);
684            self
685        }
686
687        /// Adds a context floor. Base protection remains independently
688        /// effective.
689        pub fn floor(&mut self, floor: RedactionFloor) -> &mut Self {
690            self.builder.floor_mut(self.context, floor);
691            self
692        }
693
694        /// Disables this context's explicit floor.
695        pub fn disable_floor(&mut self) -> &mut Self {
696            self.builder.disable_floor_mut(self.context);
697            self
698        }
699    }
700
701    /// Mutable view over policy limits.
702    #[must_use]
703    pub struct LimitsBuilder<'a> {
704        pub(super) builder: &'a mut RedactionPolicyBuilder,
705    }
706
707    impl LimitsBuilder<'_> {
708        /// Sets the cumulative diagnostic-event limit.
709        pub fn diagnostic_event(
710            &mut self,
711            limit: InputOutputLimit,
712        ) -> &mut Self {
713            self.builder.limits =
714                self.builder.limits.with_diagnostic_event(limit);
715            self
716        }
717
718        /// Sets the independent ordinary-operation limit.
719        pub fn ordinary_operation(
720            &mut self,
721            limit: InputOutputLimit,
722        ) -> &mut Self {
723            self.builder.limits =
724                self.builder.limits.with_ordinary_operation(limit);
725            self
726        }
727
728        /// Sets the local HTTP body limit.
729        #[cfg(feature = "http")]
730        pub fn http_body(
731            &mut self,
732            limit: crate::http::BodyBudget,
733        ) -> &mut Self {
734            self.builder.limits = self.builder.limits.with_http_body(limit);
735            self
736        }
737
738        /// Sets the JSON recursion-depth limit.
739        #[cfg(feature = "json")]
740        pub fn json_depth(&mut self, limit: JsonDepthBudget) -> &mut Self {
741            self.builder.limits =
742                self.builder.limits.with_json_depth_budget(limit);
743            self
744        }
745    }
746}
747
748#[cfg(feature = "uri")]
749pub use views::UriPolicyBuilderView;
750pub use views::{
751    FieldsBuilder,
752    LimitsBuilder,
753};
754#[cfg(feature = "http")]
755pub use views::{
756    HttpContextBuilderView,
757    HttpPolicyBuilderView,
758};
759
760impl Default for RedactionPolicyBuilder {
761    fn default() -> Self {
762        Self::new()
763    }
764}