Skip to main content

wekan_cli/
redaction.rs

1use secrecy::{ExposeSecret, SecretString};
2
3#[derive(Default)]
4pub struct Redactor<'a> {
5    secrets: Vec<&'a SecretString>,
6}
7
8impl<'a> Redactor<'a> {
9    pub fn with_secret(secret: &'a SecretString) -> Self {
10        Self {
11            secrets: vec![secret],
12        }
13    }
14
15    pub fn and_secret(mut self, secret: &'a SecretString) -> Self {
16        self.secrets.push(secret);
17        self
18    }
19
20    pub fn redact(&self, value: &str) -> String {
21        let mut secrets: Vec<_> = self
22            .secrets
23            .iter()
24            .map(|secret| secret.expose_secret())
25            .filter(|secret| !secret.is_empty())
26            .collect();
27        secrets.sort_unstable_by_key(|secret| std::cmp::Reverse(secret.len()));
28
29        secrets.into_iter().fold(value.to_owned(), |text, secret| {
30            text.replace(secret, "[REDACTED]")
31        })
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use secrecy::SecretString;
38
39    use super::Redactor;
40
41    #[test]
42    fn redacts_all_occurrences_without_exposing_debug_values() {
43        let secret = SecretString::from("do not print me".to_owned());
44        let redactor = Redactor::with_secret(&secret);
45        let result = redactor.redact("do not print me / do not print me");
46
47        assert_eq!(result, "[REDACTED] / [REDACTED]");
48        assert!(!format!("{secret:?}").contains("do not print me"));
49    }
50
51    #[test]
52    fn redacts_overlapping_secrets_without_leaking_a_suffix() {
53        let password = SecretString::from("123".to_owned());
54        let code = SecretString::from("123456".to_owned());
55        let redactor = Redactor::with_secret(&password).and_secret(&code);
56
57        assert_eq!(
58            redactor.redact("code 123456 was rejected"),
59            "code [REDACTED] was rejected"
60        );
61    }
62}