Skip to main content

swf_runtime/
secret.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use std::sync::Mutex;
4
5/// Trait for providing secrets to workflow expressions ($secret variable)
6///
7/// Implement this trait to provide secret values that can be accessed
8/// via the `$secret` expression variable in workflow definitions.
9///
10/// # Example
11///
12/// ```
13/// use swf_runtime::secret::SecretManager;
14/// use serde_json::Value;
15/// use std::collections::HashMap;
16///
17/// struct EnvSecretManager;
18///
19/// impl SecretManager for EnvSecretManager {
20///     fn get_secret(&self, key: &str) -> Option<Value> {
21///         std::env::var(key)
22///             .ok()
23///             .map(|v| Value::String(v))
24///     }
25///
26///     fn get_all_secrets(&self) -> Value {
27///         let mut map = serde_json::Map::new();
28///         for (key, value) in std::env::vars() {
29///             map.insert(key, Value::String(value));
30///         }
31///         Value::Object(map)
32///     }
33/// }
34/// ```
35pub trait SecretManager: Send + Sync {
36    /// Gets a secret value by key path (e.g., "superman.name")
37    fn get_secret(&self, key: &str) -> Option<Value>;
38
39    /// Gets all available secrets as a JSON value
40    ///
41    /// The returned value should be a JSON object that can be navigated
42    /// using dot notation in expressions (e.g., `$secret.superman.name`)
43    fn get_all_secrets(&self) -> Value;
44}
45
46/// A simple secret manager that stores secrets in a HashMap
47#[derive(Debug, Default)]
48pub struct MapSecretManager {
49    secrets: HashMap<String, Value>,
50    cached_all: Mutex<Option<Value>>,
51}
52
53impl MapSecretManager {
54    /// Creates a new empty MapSecretManager
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Adds a secret key-value pair
60    pub fn with_secret(mut self, key: impl Into<String>, value: Value) -> Self {
61        self.secrets.insert(key.into(), value);
62        *self.cached_all.get_mut().unwrap_or_else(|e| e.into_inner()) = None;
63        self
64    }
65
66    /// Sets a secret value
67    pub fn set_secret(&mut self, key: impl Into<String>, value: Value) {
68        self.secrets.insert(key.into(), value);
69        *self.cached_all.get_mut().unwrap_or_else(|e| e.into_inner()) = None;
70    }
71}
72
73impl SecretManager for MapSecretManager {
74    fn get_secret(&self, key: &str) -> Option<Value> {
75        // Support dot-notation key lookup (e.g., "superman.name" -> nested access)
76        let parts: Vec<&str> = key.split('.').collect();
77        let first = self.secrets.get(parts[0])?;
78        if parts.len() == 1 {
79            return Some(first.clone());
80        }
81        let mut current = first;
82        for part in &parts[1..] {
83            match current {
84                Value::Object(map) => {
85                    current = map.get(*part)?;
86                }
87                _ => return None,
88            }
89        }
90        Some(current.clone())
91    }
92
93    fn get_all_secrets(&self) -> Value {
94        let mut cache = self.cached_all.lock().unwrap_or_else(|e| e.into_inner());
95        if let Some(ref cached) = *cache {
96            return cached.clone();
97        }
98        let mut map = serde_json::Map::new();
99        for (key, value) in &self.secrets {
100            map.insert(key.clone(), value.clone());
101        }
102        let result = Value::Object(map);
103        *cache = Some(result.clone());
104        result
105    }
106}
107
108/// A secret manager that reads from environment variables
109#[derive(Debug, Default)]
110pub struct EnvSecretManager {
111    prefix: Option<String>,
112    cached_all: Mutex<Option<Value>>,
113}
114
115impl EnvSecretManager {
116    /// Creates a new EnvSecretManager without prefix
117    ///
118    /// **Warning:** Without a prefix, all environment variables (including sensitive ones
119    /// like PATH, HOME, AWS_SECRET_ACCESS_KEY, etc.) will be exposed as `$secret`
120    /// expression variables. Prefer [`with_prefix`](Self::with_prefix) in production.
121    #[deprecated(
122        since = "1.0.0-alpha7",
123        note = "Use `EnvSecretManager::with_prefix(\"WORKFLOW_SECRET_\")` instead to avoid exposing all environment variables"
124    )]
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Creates a new EnvSecretManager with a prefix filter
130    pub fn with_prefix(prefix: impl Into<String>) -> Self {
131        Self {
132            prefix: Some(prefix.into()),
133            cached_all: Mutex::new(None),
134        }
135    }
136}
137
138impl SecretManager for EnvSecretManager {
139    fn get_secret(&self, key: &str) -> Option<Value> {
140        std::env::var(key).ok().map(Value::String)
141    }
142
143    fn get_all_secrets(&self) -> Value {
144        let mut cache = self.cached_all.lock().unwrap_or_else(|e| e.into_inner());
145        if let Some(ref cached) = *cache {
146            return cached.clone();
147        }
148        let mut map = serde_json::Map::new();
149        for (key, value) in std::env::vars() {
150            if let Some(ref prefix) = self.prefix {
151                if key.starts_with(prefix) {
152                    map.insert(key, Value::String(value));
153                }
154            } else {
155                map.insert(key, Value::String(value));
156            }
157        }
158        let result = Value::Object(map);
159        *cache = Some(result.clone());
160        result
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use serde_json::json;
168
169    #[test]
170    fn test_map_secret_manager_simple() {
171        let mgr = MapSecretManager::new().with_secret("api_key", json!("secret123"));
172
173        assert_eq!(mgr.get_secret("api_key"), Some(json!("secret123")));
174        assert_eq!(mgr.get_secret("nonexistent"), None);
175    }
176
177    #[test]
178    fn test_map_secret_manager_nested() {
179        let mgr = MapSecretManager::new().with_secret(
180            "superman",
181            json!({
182                "name": "ClarkKent",
183                "enemy": {
184                    "name": "Lex Luthor",
185                    "isHuman": true
186                }
187            }),
188        );
189
190        assert_eq!(mgr.get_secret("superman.name"), Some(json!("ClarkKent")));
191        assert_eq!(
192            mgr.get_secret("superman.enemy.name"),
193            Some(json!("Lex Luthor"))
194        );
195        assert_eq!(mgr.get_secret("superman.enemy.isHuman"), Some(json!(true)));
196    }
197
198    #[test]
199    fn test_map_secret_manager_get_all() {
200        let mgr = MapSecretManager::new()
201            .with_secret("key1", json!("value1"))
202            .with_secret("key2", json!("value2"));
203
204        let all = mgr.get_all_secrets();
205        assert_eq!(all["key1"], json!("value1"));
206        assert_eq!(all["key2"], json!("value2"));
207    }
208}