1use std::borrow::Cow;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, LazyLock, RwLock};
17
18pub const PREFIX: &str = "DRE_SECRET_";
19pub const MASK: &str = "*****";
20pub const MASKED_SOURCE_ERROR: &str =
21 "source error details omitted because the request contains a masked secret";
22const MIN_LEN: usize = 4;
23
24static ENABLED: AtomicBool = AtomicBool::new(true);
25
26fn eligible(value: &str) -> bool {
27 value.chars().count() >= MIN_LEN
28}
29
30static SECRETS: LazyLock<RwLock<Arc<Secrets>>> = LazyLock::new(|| {
33 RwLock::new(Arc::new(Secrets::new(sorted(
34 std::env::vars()
35 .filter(|(k, _)| k.starts_with(PREFIX))
36 .map(|(_, v)| v),
37 ))))
38});
39
40struct Secrets {
42 plain: Vec<String>,
43 markdown: Vec<String>,
44 title: Vec<String>,
45}
46
47impl Secrets {
48 fn new(plain: Vec<String>) -> Secrets {
49 let markdown = sorted(
50 plain
51 .iter()
52 .flat_map(|s| [s.clone(), dre_protocol::markdown::escape(s)]),
53 );
54 let title = sorted(
55 plain
56 .iter()
57 .flat_map(|s| [s.clone(), s.split_whitespace().collect::<Vec<_>>().join(" ")]),
58 );
59 Secrets {
60 plain,
61 markdown,
62 title,
63 }
64 }
65}
66
67fn sorted(values: impl IntoIterator<Item = String>) -> Vec<String> {
68 let mut v: Vec<String> = values.into_iter().filter(|v| eligible(v)).collect();
69 v.sort_by_key(|s| std::cmp::Reverse(s.len()));
70 v.dedup();
71 v
72}
73
74fn secrets() -> Arc<Secrets> {
75 SECRETS.read().unwrap_or_else(|e| e.into_inner()).clone()
76}
77
78pub fn env_references(text: &str) -> Vec<String> {
80 static CALL: LazyLock<regex::Regex> =
81 LazyLock::new(|| regex::Regex::new(r#"env_var\(\s*(?:'([^']+)'|"([^"]+)")"#).unwrap());
82 CALL.captures_iter(text)
83 .filter_map(|c| c.get(1).or_else(|| c.get(2)))
84 .map(|m| m.as_str().to_owned())
85 .collect()
86}
87
88pub fn register_env_references(text: &str) {
91 let values: Vec<String> = env_references(text)
92 .into_iter()
93 .filter_map(|name| std::env::var(name).ok())
94 .collect();
95 if values.is_empty() {
96 return;
97 }
98 let mut guard = SECRETS.write().unwrap_or_else(|e| e.into_inner());
99 let merged = sorted(guard.plain.iter().cloned().chain(values));
100 *guard = Arc::new(Secrets::new(merged));
101}
102
103pub fn set_enabled(on: bool) {
104 ENABLED.store(on, Ordering::Relaxed);
105}
106
107pub(crate) fn enabled() -> bool {
108 ENABLED.load(Ordering::Relaxed)
109}
110
111fn mask_with<'a>(s: &'a str, secrets: &[String]) -> Cow<'a, str> {
112 if !secrets.iter().any(|x| s.contains(x.as_str())) {
113 return Cow::Borrowed(s);
114 }
115 let mut out = s.to_string();
116 for x in secrets {
117 out = out.replace(x.as_str(), MASK);
118 }
119 Cow::Owned(out)
120}
121
122pub fn mask(s: &str) -> Cow<'_, str> {
124 if !ENABLED.load(Ordering::Relaxed) {
125 return Cow::Borrowed(s);
126 }
127 mask_with(s, &secrets().plain)
128}
129
130pub(crate) fn mask_markdown(s: &str) -> Cow<'_, str> {
133 if !ENABLED.load(Ordering::Relaxed) {
134 return Cow::Borrowed(s);
135 }
136 mask_with(s, &secrets().markdown)
137}
138
139pub(crate) fn mask_message_title(s: &str) -> Cow<'_, str> {
142 if !ENABLED.load(Ordering::Relaxed) {
143 return Cow::Borrowed(s);
144 }
145 mask_with(s, &secrets().title)
146}
147
148pub fn contains_secret(s: &str) -> bool {
150 ENABLED.load(Ordering::Relaxed) && secrets().plain.iter().any(|secret| s.contains(secret.as_str()))
151}
152
153fn mask_source_error_with<'a>(error: &'a str, sensitive: bool, secrets: &[String]) -> Cow<'a, str> {
154 if sensitive {
155 Cow::Borrowed(MASKED_SOURCE_ERROR)
156 } else {
157 mask_with(error, secrets)
158 }
159}
160
161pub fn mask_source_error(error: &str, sensitive: bool) -> Cow<'_, str> {
165 if !ENABLED.load(Ordering::Relaxed) {
166 return Cow::Borrowed(error);
167 }
168 mask_source_error_with(error, sensitive, &secrets().plain)
169}
170
171fn unique_masked_names_with(names: &[String], secrets: &[String]) -> Vec<String> {
172 use std::collections::BTreeSet;
173
174 let masked: Vec<String> = names
175 .iter()
176 .map(|name| mask_with(name, secrets).into_owned())
177 .collect();
178 if names == masked {
179 return names.to_vec();
180 }
181 let mut used = BTreeSet::new();
182 let mut output = vec![String::new(); names.len()];
183 let mut pending = Vec::new();
184
185 for (index, (original, protected)) in names.iter().zip(&masked).enumerate() {
187 if original == protected {
188 used.insert(original.clone());
189 output[index].clone_from(original);
190 } else {
191 pending.push((protected.clone(), original.clone(), index));
192 }
193 }
194
195 pending.sort();
197 for (base, _, index) in pending {
198 let mut candidate = base.clone();
199 let mut suffix = 2;
200 while !used.insert(candidate.clone()) {
201 candidate = format!("{base}#{suffix}");
202 suffix += 1;
203 }
204 output[index] = candidate;
205 }
206 output
207}
208
209fn redact_json_with(v: &mut serde_json::Value, secrets: &[String]) {
210 use serde_json::Value;
211 match v {
212 Value::String(s) => {
213 if let Cow::Owned(m) = mask_with(s, secrets) {
214 *s = m;
215 }
216 }
217 Value::Array(a) => a.iter_mut().for_each(|v| redact_json_with(v, secrets)),
218 Value::Object(o) => {
219 let entries = std::mem::take(o);
220 let keys: Vec<String> = entries.keys().cloned().collect();
221 let protected_keys = unique_masked_names_with(&keys, secrets);
222 for ((_, mut val), protected_key) in entries.into_iter().zip(protected_keys) {
223 redact_json_with(&mut val, secrets);
224 o.insert(protected_key, val);
225 }
226 }
227 _ => {}
228 }
229}
230
231pub fn redact_json(v: &mut serde_json::Value) {
234 if ENABLED.load(Ordering::Relaxed) {
235 redact_json_with(v, &secrets().plain);
236 }
237}
238
239fn redacted<T: serde::Serialize>(v: &T) -> serde_json::Result<serde_json::Value> {
240 let mut v = serde_json::to_value(v)?;
241 redact_json(&mut v);
242 Ok(v)
243}
244
245pub fn to_json_pretty<T: serde::Serialize>(v: &T) -> serde_json::Result<String> {
247 serde_json::to_string_pretty(&redacted(v)?)
248}
249
250pub fn to_json_line<T: serde::Serialize>(v: &T) -> serde_json::Result<String> {
252 serde_json::to_string(&redacted(v)?)
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use serde_json::json;
259
260 fn secrets(values: &[&str]) -> Vec<String> {
261 let mut values: Vec<String> = values.iter().map(|v| (*v).to_string()).collect();
262 values.sort_by_key(|s| std::cmp::Reverse(s.len()));
263 values
264 }
265
266 #[test]
267 fn redacts_nested_values_longest_first() {
268 let secrets = secrets(&["nested-secret", "nested-secret-and-more"]);
269 let mut value = json!({"items": ["nested-secret-and-more", {"value": "x nested-secret y"}]});
270 redact_json_with(&mut value, &secrets);
271 assert_eq!(value, json!({"items": ["*****", {"value": "x ***** y"}]}));
272 }
273
274 #[test]
275 fn preserves_entries_when_masked_keys_collide() {
276 let secrets = secrets(&["alpha-secret-key", "bravo-secret-key"]);
277 let mut value = json!({
278 "alpha-secret-key": 1,
279 "bravo-secret-key": 2,
280 "*****": 3,
281 "*****#2": 4,
282 });
283 redact_json_with(&mut value, &secrets);
284 assert_eq!(
285 value,
286 json!({
287 "*****": 3,
288 "*****#2": 4,
289 "*****#3": 1,
290 "*****#4": 2,
291 })
292 );
293 }
294
295 #[test]
296 fn masking_one_name_does_not_rename_duplicate_literal_names() {
297 let secrets = secrets(&["secret-column"]);
298 let names = ["a", "a", "secret-column"].map(str::to_string);
299 assert_eq!(unique_masked_names_with(&names, &secrets), ["a", "a", "*****"]);
300 }
301
302 #[test]
303 fn suppresses_transformed_source_errors_only_for_secret_requests() {
304 let secrets = secrets(&["long-secret-value-that-a-source-truncated"]);
305 let error = "LINE 1: select * from long-secret-value-that-a-source...";
306 assert_eq!(mask_source_error_with(error, true, &secrets), MASKED_SOURCE_ERROR);
307 assert_eq!(
308 mask_source_error_with("ordinary syntax error", false, &secrets),
309 "ordinary syntax error"
310 );
311 }
312
313 #[test]
314 fn finds_env_var_references_with_either_quote() {
315 assert_eq!(
316 env_references(
317 "password: \"{{ env_var('PG_PASSWORD') }}\"\ntoken: '{{ env_var(\"WAREHOUSE_TOKEN\", \"x\") }}'"
318 ),
319 ["PG_PASSWORD", "WAREHOUSE_TOKEN"]
320 );
321 }
322
323 #[test]
324 fn minimum_length_counts_unicode_characters() {
325 assert!(!eligible("ééé"));
326 assert!(eligible("éééé"));
327 }
328}