openzeppelin_relayer/models/
plain_or_env_value.rs

1//! PlainOrEnvValue module for secure configuration value handling
2//!
3//! This module provides functionality to securely handle configuration values
4//! that can either be provided directly in the configuration file ("plain")
5//! or retrieved from environment variables ("env").
6//!
7//! The `PlainOrEnvValue` enum supports two variants:
8//! - `Plain`: For values stored directly in the configuration
9//! - `Env`: For values that should be retrieved from environment variables
10//!
11//! When a value is requested, if it's an "env" variant, the module will
12//! attempt to retrieve the value from the specified environment variable.
13//! All values are wrapped in `SecretString` to ensure secure memory handling.
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use validator::ValidationError;
17use zeroize::Zeroizing;
18
19use super::SecretString;
20
21#[derive(Error, Debug)]
22pub enum PlainOrEnvValueError {
23    #[error("Missing env var: {0}")]
24    MissingEnvVar(String),
25}
26
27#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
28#[serde(tag = "type", rename_all = "lowercase")]
29pub enum PlainOrEnvValue {
30    Env { value: String },
31    Plain { value: SecretString },
32}
33
34impl PlainOrEnvValue {
35    pub fn get_value(&self) -> Result<SecretString, PlainOrEnvValueError> {
36        match self {
37            PlainOrEnvValue::Env { value } => {
38                let value = Zeroizing::new(std::env::var(value).map_err(|_| {
39                    PlainOrEnvValueError::MissingEnvVar(format!(
40                        "Environment variable {} not found",
41                        value
42                    ))
43                })?);
44                Ok(SecretString::new(&value))
45            }
46            PlainOrEnvValue::Plain { value } => Ok(value.clone()),
47        }
48    }
49    pub fn is_empty(&self) -> bool {
50        let value = self.get_value();
51
52        match value {
53            Ok(v) => v.is_empty(),
54            Err(_) => true,
55        }
56    }
57}
58
59pub fn validate_plain_or_env_value(plain_or_env: &PlainOrEnvValue) -> Result<(), ValidationError> {
60    let value = plain_or_env.get_value().map_err(|e| {
61        let mut err = ValidationError::new("plain_or_env_value_error");
62        err.message = Some(format!("plain_or_env_value_error: {}", e).into());
63        err
64    })?;
65
66    match value.is_empty() {
67        true => Err(ValidationError::new(
68            "plain_or_env_value_error: value cannot be empty",
69        )),
70        false => Ok(()),
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use std::{env, sync::Mutex};
78    use validator::Validate;
79
80    static ENV_MUTEX: Mutex<()> = Mutex::new(());
81
82    #[derive(Validate)]
83    struct TestStruct {
84        #[validate(custom(function = "validate_plain_or_env_value"))]
85        value: PlainOrEnvValue,
86    }
87
88    #[test]
89    fn test_plain_value_get_value() {
90        let plain = PlainOrEnvValue::Plain {
91            value: SecretString::new("test-secret"),
92        };
93
94        let result = plain.get_value().unwrap();
95        result.as_str(|s| {
96            assert_eq!(s, "test-secret");
97        });
98    }
99
100    #[test]
101    fn test_env_value_get_value_when_env_exists() {
102        let _guard = ENV_MUTEX
103            .lock()
104            .unwrap_or_else(|poisoned| poisoned.into_inner());
105
106        env::set_var("TEST_ENV_VAR", "env-secret-value");
107
108        let env_value = PlainOrEnvValue::Env {
109            value: "TEST_ENV_VAR".to_string(),
110        };
111
112        let result = env_value.get_value().unwrap();
113        result.as_str(|s| {
114            assert_eq!(s, "env-secret-value");
115        });
116
117        env::remove_var("TEST_ENV_VAR");
118    }
119
120    #[test]
121    fn test_env_value_get_value_when_env_missing() {
122        let _guard = ENV_MUTEX
123            .lock()
124            .unwrap_or_else(|poisoned| poisoned.into_inner());
125
126        env::remove_var("NONEXISTENT_VAR");
127
128        let env_value = PlainOrEnvValue::Env {
129            value: "NONEXISTENT_VAR".to_string(),
130        };
131
132        let result = env_value.get_value();
133        assert!(result.is_err());
134
135        match result {
136            Err(PlainOrEnvValueError::MissingEnvVar(msg)) => {
137                assert!(msg.contains("NONEXISTENT_VAR"));
138            }
139            _ => panic!("Expected MissingEnvVar error"),
140        }
141    }
142
143    #[test]
144    fn test_is_empty_with_plain_empty_value() {
145        let plain = PlainOrEnvValue::Plain {
146            value: SecretString::new(""),
147        };
148
149        assert!(plain.is_empty());
150    }
151
152    #[test]
153    fn test_is_empty_with_plain_non_empty_value() {
154        let plain = PlainOrEnvValue::Plain {
155            value: SecretString::new("non-empty"),
156        };
157
158        assert!(!plain.is_empty());
159    }
160
161    #[test]
162    fn test_is_empty_with_env_missing_var() {
163        let _guard = ENV_MUTEX
164            .lock()
165            .unwrap_or_else(|poisoned| poisoned.into_inner());
166
167        env::remove_var("NONEXISTENT_VAR");
168
169        let env_value = PlainOrEnvValue::Env {
170            value: "NONEXISTENT_VAR".to_string(),
171        };
172
173        assert!(env_value.is_empty());
174    }
175
176    #[test]
177    fn test_is_empty_with_env_empty_var() {
178        let _guard = ENV_MUTEX
179            .lock()
180            .unwrap_or_else(|poisoned| poisoned.into_inner());
181
182        env::set_var("EMPTY_ENV_VAR", "");
183
184        let env_value = PlainOrEnvValue::Env {
185            value: "EMPTY_ENV_VAR".to_string(),
186        };
187
188        assert!(env_value.is_empty());
189
190        env::remove_var("EMPTY_ENV_VAR");
191    }
192
193    #[test]
194    fn test_is_empty_with_env_non_empty_var() {
195        let _guard = ENV_MUTEX
196            .lock()
197            .unwrap_or_else(|poisoned| poisoned.into_inner());
198
199        env::set_var("TEST_ENV_VAR", "some-value");
200
201        let env_value = PlainOrEnvValue::Env {
202            value: "TEST_ENV_VAR".to_string(),
203        };
204
205        assert!(!env_value.is_empty());
206
207        env::remove_var("TEST_ENV_VAR");
208    }
209
210    #[test]
211    fn test_validator_with_plain_empty_value() {
212        let test_struct = TestStruct {
213            value: PlainOrEnvValue::Plain {
214                value: SecretString::new(""),
215            },
216        };
217
218        let result = test_struct.validate();
219        assert!(result.is_err());
220    }
221
222    #[test]
223    fn test_validator_with_plain_non_empty_value() {
224        let test_struct = TestStruct {
225            value: PlainOrEnvValue::Plain {
226                value: SecretString::new("non-empty"),
227            },
228        };
229
230        let result = test_struct.validate();
231        assert!(result.is_ok());
232    }
233
234    #[test]
235    fn test_validator_with_env_missing_var() {
236        let _guard = ENV_MUTEX
237            .lock()
238            .unwrap_or_else(|poisoned| poisoned.into_inner());
239
240        env::remove_var("NONEXISTENT_VAR");
241
242        let test_struct = TestStruct {
243            value: PlainOrEnvValue::Env {
244                value: "NONEXISTENT_VAR".to_string(),
245            },
246        };
247
248        let result = test_struct.validate();
249        assert!(result.is_err());
250    }
251
252    #[test]
253    fn test_validator_with_env_empty_var() {
254        let _guard = ENV_MUTEX
255            .lock()
256            .unwrap_or_else(|poisoned| poisoned.into_inner());
257
258        env::set_var("EMPTY_ENV_VAR", "");
259
260        let test_struct = TestStruct {
261            value: PlainOrEnvValue::Env {
262                value: "EMPTY_ENV_VAR".to_string(),
263            },
264        };
265
266        let result = test_struct.validate();
267        assert!(result.is_err());
268
269        env::remove_var("EMPTY_ENV_VAR");
270    }
271
272    #[test]
273    fn test_validator_with_env_non_empty_var() {
274        let _guard = ENV_MUTEX
275            .lock()
276            .unwrap_or_else(|poisoned| poisoned.into_inner());
277
278        env::set_var("TEST_ENV_VAR", "some-value");
279
280        let test_struct = TestStruct {
281            value: PlainOrEnvValue::Env {
282                value: "TEST_ENV_VAR".to_string(),
283            },
284        };
285
286        let result = test_struct.validate();
287        assert!(result.is_ok());
288
289        env::remove_var("TEST_ENV_VAR");
290    }
291
292    #[test]
293    fn test_serialize_plain_value() {
294        let plain = PlainOrEnvValue::Plain {
295            value: SecretString::new("test-secret"),
296        };
297
298        let serialized = serde_json::to_string(&plain).unwrap();
299
300        assert!(serialized.contains(r#""type":"plain"#));
301        assert!(serialized.contains(r#""value":"REDACTED"#));
302    }
303
304    #[test]
305    fn test_serialize_env_value() {
306        let env_value = PlainOrEnvValue::Env {
307            value: "TEST_ENV_VAR".to_string(),
308        };
309
310        let serialized = serde_json::to_string(&env_value).unwrap();
311
312        assert!(serialized.contains(r#""type":"env"#));
313        assert!(serialized.contains(r#""value":"TEST_ENV_VAR"#));
314    }
315
316    #[test]
317    fn test_deserialize_plain_value() {
318        let json = r#"{"type":"plain","value":"test-secret"}"#;
319
320        let deserialized: PlainOrEnvValue = serde_json::from_str(json).unwrap();
321
322        match &deserialized {
323            PlainOrEnvValue::Plain { value } => {
324                value.as_str(|s| {
325                    assert_eq!(s, "test-secret");
326                });
327            }
328            _ => panic!("Expected Plain variant"),
329        }
330    }
331
332    #[test]
333    fn test_deserialize_env_value() {
334        let json = r#"{"type":"env","value":"TEST_ENV_VAR"}"#;
335
336        let deserialized: PlainOrEnvValue = serde_json::from_str(json).unwrap();
337
338        match &deserialized {
339            PlainOrEnvValue::Env { value } => {
340                assert_eq!(value, "TEST_ENV_VAR");
341            }
342            _ => panic!("Expected Env variant"),
343        }
344    }
345
346    #[test]
347    fn test_error_messages() {
348        let error = PlainOrEnvValueError::MissingEnvVar("TEST_VAR".to_string());
349        let message = format!("{}", error);
350        assert_eq!(message, "Missing env var: TEST_VAR");
351    }
352
353    #[test]
354    fn test_validation_error_messages() {
355        let test_struct = TestStruct {
356            value: PlainOrEnvValue::Plain {
357                value: SecretString::new(""),
358            },
359        };
360
361        let result = test_struct.validate();
362        assert!(result.is_err());
363
364        if let Err(errors) = result {
365            let field_errors = errors.field_errors();
366            assert!(field_errors.contains_key("value"));
367
368            let error_msgs = &field_errors["value"];
369            assert!(!error_msgs.is_empty());
370
371            let has_empty_message = error_msgs
372                .iter()
373                .any(|e| e.code == "plain_or_env_value_error: value cannot be empty");
374
375            assert!(
376                has_empty_message,
377                "Validation error should mention empty value"
378            );
379        }
380    }
381}