Skip to main content

dnslib/core/
redaction.rs

1use secrecy::{ExposeSecret, SecretString};
2use serde_json::Value;
3
4const SENSITIVE_KEY_PARTS: &[&str] = &[
5    "secret",
6    "password",
7    "token",
8    "apikey",
9    "privatekey",
10    "sharedsecret",
11];
12
13/// Marker used in API responses when a field exists but its value is sensitive.
14pub const REDACTED_MARKER: &str = "[redacted]";
15
16/// Recursively redact sensitive fields in a JSON value while preserving shape.
17pub fn redact_sensitive_fields(mut value: Value) -> Value {
18    redact_sensitive_fields_in_place(&mut value);
19    value
20}
21
22/// Recursively redact sensitive fields in a JSON value while preserving shape.
23pub fn redact_sensitive_fields_in_place(value: &mut Value) {
24    match value {
25        Value::Object(map) => {
26            for (key, value) in map {
27                if is_sensitive_key(key) {
28                    *value = redacted_value();
29                } else {
30                    redact_sensitive_fields_in_place(value);
31                }
32            }
33        }
34        Value::Array(values) => {
35            for value in values {
36                redact_sensitive_fields_in_place(value);
37            }
38        }
39        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
40    }
41}
42
43fn is_sensitive_key(key: &str) -> bool {
44    let normalized = normalize_key(key);
45    SENSITIVE_KEY_PARTS
46        .iter()
47        .any(|part| normalized.contains(part))
48}
49
50fn normalize_key(key: &str) -> String {
51    key.chars()
52        .filter(|c| c.is_ascii_alphanumeric())
53        .flat_map(char::to_lowercase)
54        .collect()
55}
56
57fn redacted_value() -> Value {
58    let marker = SecretString::from(REDACTED_MARKER.to_owned());
59    Value::String(marker.expose_secret().to_owned())
60}
61
62#[cfg(test)]
63mod tests {
64    use serde_json::json;
65
66    use super::*;
67
68    #[test]
69    fn redacts_nested_object_fields() {
70        let settings = json!({
71            "response": {
72                "tsigKeys": [
73                    {
74                        "name": "zone-transfer",
75                        "sharedSecret": "actual-secret"
76                    }
77                ]
78            }
79        });
80
81        let redacted = redact_sensitive_fields(settings);
82
83        assert_eq!(
84            redacted["response"]["tsigKeys"][0]["sharedSecret"],
85            REDACTED_MARKER
86        );
87        assert_eq!(redacted["response"]["tsigKeys"][0]["name"], "zone-transfer");
88    }
89
90    #[test]
91    fn redacts_password_fields_even_when_already_masked() {
92        let settings = json!({
93            "dnsTlsCertificatePassword": "********",
94            "webServiceTlsCertificatePassword": ""
95        });
96
97        let redacted = redact_sensitive_fields(settings);
98
99        assert_eq!(redacted["dnsTlsCertificatePassword"], REDACTED_MARKER);
100        assert_eq!(
101            redacted["webServiceTlsCertificatePassword"],
102            REDACTED_MARKER
103        );
104    }
105
106    #[test]
107    fn leaves_unrelated_fields_unchanged() {
108        let settings = json!({
109            "version": "13.4.1",
110            "clusterDomain": "cluster.example.test",
111            "dnsServerDomain": "dns.example.test"
112        });
113
114        let redacted = redact_sensitive_fields(settings.clone());
115
116        assert_eq!(redacted, settings);
117    }
118
119    #[test]
120    fn redacts_arrays_of_objects() {
121        let settings = json!({
122            "providers": [
123                { "apiKey": "one", "name": "primary" },
124                { "api_token": "two", "name": "secondary" },
125                { "private-key": "three", "name": "tertiary" }
126            ]
127        });
128
129        let redacted = redact_sensitive_fields(settings);
130
131        assert_eq!(redacted["providers"][0]["apiKey"], REDACTED_MARKER);
132        assert_eq!(redacted["providers"][1]["api_token"], REDACTED_MARKER);
133        assert_eq!(redacted["providers"][2]["private-key"], REDACTED_MARKER);
134        assert_eq!(redacted["providers"][2]["name"], "tertiary");
135    }
136}