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