Skip to main content

relay_pii/
config.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::sync::OnceLock;
4
5use regex::{Regex, RegexBuilder};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::{CompiledPiiConfig, Redaction, SelectorSpec};
9
10const COMPILED_PATTERN_MAX_SIZE: usize = 262_144;
11
12/// Helper method to check whether a flag is false.
13#[allow(clippy::trivially_copy_pass_by_ref)]
14pub(crate) fn is_flag_default(flag: &bool) -> bool {
15    !*flag
16}
17
18/// An error returned when parsing [`PiiConfig`].
19#[derive(Clone, Debug, thiserror::Error)]
20pub enum PiiConfigError {
21    /// A match pattern in a PII rule config could not be parsed.
22    #[error("could not parse pattern")]
23    RegexError(#[source] regex::Error),
24}
25
26/// Wrapper for the regex and the raw pattern string.
27///
28/// The regex will be compiled only when it used once, and the compiled version will be reused on
29/// consecutive calls.
30#[derive(Debug, Clone)]
31pub struct LazyPattern {
32    raw: Cow<'static, str>,
33    case_insensitive: bool,
34    pattern: OnceLock<Result<Regex, PiiConfigError>>,
35}
36
37impl PartialEq for LazyPattern {
38    fn eq(&self, other: &Self) -> bool {
39        self.raw.to_lowercase() == other.raw.to_lowercase()
40    }
41}
42
43impl LazyPattern {
44    /// Create a new [`LazyPattern`] from a raw string.
45    pub fn new<S>(raw: S) -> Self
46    where
47        Cow<'static, str>: From<S>,
48    {
49        Self {
50            raw: raw.into(),
51            case_insensitive: false,
52            pattern: OnceLock::new(),
53        }
54    }
55
56    /// Change the case sensativity settings for the underlying regex.
57    ///
58    /// It's possible to set the case sensativity on already compiled [`LazyPattern`], which will
59    /// be recompiled (re-built) once it's used again.
60    pub fn case_insensitive(mut self, value: bool) -> Self {
61        self.case_insensitive = value;
62        self.pattern.take();
63        self
64    }
65
66    /// Compiles the regex from the internal raw string.
67    pub fn compiled(&self) -> Result<&Regex, &PiiConfigError> {
68        self.pattern
69            .get_or_init(|| {
70                let regex_result = RegexBuilder::new(&self.raw)
71                    .size_limit(COMPILED_PATTERN_MAX_SIZE)
72                    .case_insensitive(self.case_insensitive)
73                    .build()
74                    .map_err(PiiConfigError::RegexError);
75
76                if let Err(ref error) = regex_result {
77                    relay_log::error!(
78                        error = error as &dyn std::error::Error,
79                        "unable to compile pattern into regex"
80                    );
81                }
82                regex_result
83            })
84            .as_ref()
85    }
86}
87
88impl From<&'static str> for LazyPattern {
89    fn from(pattern: &'static str) -> LazyPattern {
90        LazyPattern::new(pattern)
91    }
92}
93
94impl Serialize for LazyPattern {
95    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96        serializer.serialize_str(&self.raw)
97    }
98}
99
100impl<'de> Deserialize<'de> for LazyPattern {
101    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
102        let raw = String::deserialize(deserializer)?;
103        Ok(LazyPattern::new(raw))
104    }
105}
106
107#[allow(clippy::unnecessary_wraps)]
108fn replace_groups_default() -> Option<BTreeSet<u8>> {
109    let mut set = BTreeSet::new();
110    set.insert(0);
111    Some(set)
112}
113
114/// A rule that matches a regex pattern.
115#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
116#[serde(rename_all = "camelCase")]
117pub struct PatternRule {
118    /// The regular expression to apply.
119    pub pattern: LazyPattern,
120    /// The match group indices to replace.
121    #[serde(default = "replace_groups_default")]
122    pub replace_groups: Option<BTreeSet<u8>>,
123}
124
125/// A rule that dispatches to multiple other rules.
126#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct MultipleRule {
129    /// A reference to other rules to apply
130    pub rules: Vec<String>,
131    /// When set to true, the outer rule is reported.
132    #[serde(default, skip_serializing_if = "is_flag_default")]
133    pub hide_inner: bool,
134}
135
136/// An alias for another rule.
137#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
138#[serde(rename_all = "camelCase")]
139pub struct AliasRule {
140    /// A reference to another rule to apply.
141    pub rule: String,
142    /// When set to true, the outer rule is reported.
143    #[serde(default, skip_serializing_if = "is_flag_default")]
144    pub hide_inner: bool,
145}
146
147/// A pair redaction rule.
148#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub struct RedactPairRule {
151    /// A pattern to match for keys.
152    ///
153    /// Note: despite the name, this is matched against both the
154    /// key and value. The reason is that the value may itself
155    /// be (the string representation of) a complex object with
156    /// keys of its own, and we explicitly want to match those
157    /// keys as well. See the `test_breadcrumb_message` test
158    /// for an example of this.
159    pub key_pattern: LazyPattern,
160}
161
162/// Supported scrubbing rules.
163#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
164#[serde(tag = "type", rename_all = "snake_case")]
165pub enum RuleType {
166    /// Matches any value.
167    Anything,
168    /// Applies a regular expression.
169    Pattern(PatternRule),
170    /// Matchse an IMEI or IMEISV
171    Imei,
172    /// Matches a mac address
173    Mac,
174    /// Matches a UUID
175    Uuid,
176    /// Matches an email
177    Email,
178    /// Matches any IP address
179    Ip,
180    /// Matches a creditcard number
181    Creditcard,
182    /// Matches an IBAN
183    Iban,
184    /// Sanitizes a path from user data
185    Userpath,
186    /// A PEM encoded key
187    Pemkey,
188    /// Auth info from URLs
189    UrlAuth,
190    /// US SSN.
191    UsSsn,
192    /// A Bearer token
193    Bearer,
194    /// Keys that look like passwords
195    Password,
196    /// When a regex matches a key, a value is removed
197    #[serde(alias = "redactPair")]
198    RedactPair(RedactPairRule),
199    /// Applies multiple rules.
200    Multiple(MultipleRule),
201    /// Applies another rule.  Works like a single multiple.
202    Alias(AliasRule),
203    /// Unknown ruletype for forward compatibility
204    Unknown(String),
205}
206
207/// A single rule configuration.
208#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
209pub struct RuleSpec {
210    /// The matching rule to apply on fields.
211    #[serde(flatten)]
212    pub ty: RuleType,
213
214    /// The redaction to apply on matched fields.
215    #[serde(default)]
216    pub redaction: Redaction,
217}
218
219/// Configuration for rule parameters.
220#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
221#[serde(rename_all = "camelCase")]
222pub struct Vars {
223    /// The default secret key for hashing operations.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub hash_key: Option<String>,
226}
227
228impl Vars {
229    fn is_empty(&self) -> bool {
230        self.hash_key.is_none()
231    }
232}
233
234/// A set of named rule configurations.
235#[derive(Serialize, Deserialize, Debug, Default, Clone)]
236pub struct PiiConfig {
237    /// A map of custom PII rules.
238    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
239    pub rules: BTreeMap<String, RuleSpec>,
240
241    /// Parameters for PII rules.
242    #[serde(default, skip_serializing_if = "Vars::is_empty")]
243    pub vars: Vars,
244
245    /// Mapping of selectors to rules.
246    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
247    pub applications: BTreeMap<SelectorSpec, Vec<String>>,
248
249    /// PII config derived from datascrubbing settings.
250    ///
251    /// Cached because the conversion process is expensive.
252    #[serde(skip)]
253    pub(super) compiled: OnceLock<CompiledPiiConfig>,
254}
255
256impl PartialEq for PiiConfig {
257    fn eq(&self, other: &PiiConfig) -> bool {
258        // This is written in this way such that people will not forget to update this PartialEq
259        // impl when they add more fields.
260        let PiiConfig {
261            rules,
262            vars,
263            applications,
264            compiled: _compiled,
265        } = &self;
266
267        rules == &other.rules && vars == &other.vars && applications == &other.applications
268    }
269}
270
271impl PiiConfig {
272    /// Get a representation of this `PiiConfig` that is more (CPU-)efficient for processing.
273    ///
274    /// This can be computationally expensive when called for the first time. The result is cached
275    /// internally and reused on the second call.
276    pub fn compiled(&self) -> &CompiledPiiConfig {
277        self.compiled.get_or_init(|| self.compiled_uncached())
278    }
279
280    /// Like [`compiled`](Self::compiled) but without internal caching.
281    #[inline]
282    pub fn compiled_uncached(&self) -> CompiledPiiConfig {
283        CompiledPiiConfig::new(self)
284    }
285}