microsandbox_network/engine/secrets/
handle.rs1use std::sync::{Arc, RwLock};
9
10use crate::secrets::config::{HostPattern, SecretsConfig};
11
12#[derive(Clone)]
20pub struct SecretsHandle {
21 inner: Arc<RwLock<Arc<SecretsConfig>>>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
28pub enum SecretsUpdateError {
29 #[error("no secret named {name} is configured")]
31 UnknownSecret {
32 name: String,
34 },
35
36 #[error("secret {name}: at least one allowed host is required")]
38 MissingAllowedHosts {
39 name: String,
41 },
42}
43
44impl SecretsHandle {
49 pub fn new(config: SecretsConfig) -> Self {
51 Self {
52 inner: Arc::new(RwLock::new(Arc::new(config))),
53 }
54 }
55
56 pub fn load(&self) -> Arc<SecretsConfig> {
58 self.inner.read().expect("secrets lock poisoned").clone()
59 }
60
61 pub fn rotate_value(&self, name: &str, value: String) -> Result<(), SecretsUpdateError> {
64 self.update(name, |entry| {
65 entry.value = zeroize::Zeroizing::new(value);
66 })
67 }
68
69 pub fn remove(&self, name: &str) {
72 let mut guard = self.inner.write().expect("secrets lock poisoned");
73 if !guard.secrets.iter().any(|entry| entry.env_var == name) {
74 return;
75 }
76 let mut config = (**guard).clone();
77 config.secrets.retain(|entry| entry.env_var != name);
78 *guard = Arc::new(config);
79 }
80
81 pub fn set_allowed_hosts(
83 &self,
84 name: &str,
85 hosts: &[String],
86 ) -> Result<(), SecretsUpdateError> {
87 if hosts.is_empty() {
88 return Err(SecretsUpdateError::MissingAllowedHosts {
89 name: name.to_string(),
90 });
91 }
92 let hosts: Vec<HostPattern> = hosts.iter().map(|host| HostPattern::parse(host)).collect();
93 self.update(name, |entry| entry.allowed_hosts = hosts)
94 }
95
96 fn update(
98 &self,
99 name: &str,
100 mutate: impl FnOnce(&mut crate::secrets::config::SecretEntry),
101 ) -> Result<(), SecretsUpdateError> {
102 let mut guard = self.inner.write().expect("secrets lock poisoned");
103 let mut config = (**guard).clone();
104 let entry = config
105 .secrets
106 .iter_mut()
107 .find(|entry| entry.env_var == name)
108 .ok_or_else(|| SecretsUpdateError::UnknownSecret {
109 name: name.to_string(),
110 })?;
111 mutate(entry);
112 *guard = Arc::new(config);
113 Ok(())
114 }
115}
116
117#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::secrets::config::{SecretEntry, SecretSubstitution};
125
126 fn config_with_secret(name: &str, value: &str) -> SecretsConfig {
127 SecretsConfig {
128 secrets: vec![SecretEntry {
129 env_var: name.to_string(),
130 value: zeroize::Zeroizing::new(value.to_string()),
131 source: None,
132 placeholder: format!("$MSB_{name}"),
133 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
134 substitution: SecretSubstitution::default(),
135 passthrough_hosts: Vec::new(),
136 violation_action: None,
137 require_tls_identity: true,
138 }],
139 ..SecretsConfig::default()
140 }
141 }
142
143 #[test]
144 fn rotate_swaps_value_for_future_loads() {
145 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
146 let before = handle.load();
147
148 handle.rotate_value("API_KEY", "new".into()).unwrap();
149
150 assert_eq!(before.secrets[0].value.as_str(), "old");
152 assert_eq!(handle.load().secrets[0].value.as_str(), "new");
153 assert_eq!(handle.load().secrets[0].placeholder, "$MSB_API_KEY");
154 }
155
156 #[test]
157 fn rotate_unknown_secret_is_an_error() {
158 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
159
160 assert_eq!(
161 handle.rotate_value("MISSING", "new".into()),
162 Err(SecretsUpdateError::UnknownSecret {
163 name: "MISSING".into()
164 })
165 );
166 }
167
168 #[test]
169 fn remove_drops_entry_and_is_idempotent() {
170 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
171
172 handle.remove("API_KEY");
173 handle.remove("API_KEY");
174
175 assert!(handle.load().secrets.is_empty());
176 }
177
178 #[test]
179 fn set_allowed_hosts_replaces_patterns() {
180 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
181
182 handle
183 .set_allowed_hosts("API_KEY", &["*.example.org".into(), "one.test".into()])
184 .unwrap();
185
186 assert_eq!(
187 handle.load().secrets[0].allowed_hosts,
188 vec![
189 HostPattern::Wildcard("*.example.org".into()),
190 HostPattern::Exact("one.test".into()),
191 ]
192 );
193 }
194
195 #[test]
196 fn set_allowed_hosts_rejects_empty_list() {
197 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
198
199 assert_eq!(
200 handle.set_allowed_hosts("API_KEY", &[]),
201 Err(SecretsUpdateError::MissingAllowedHosts {
202 name: "API_KEY".into()
203 })
204 );
205 assert_eq!(handle.load().secrets[0].allowed_hosts.len(), 1);
206 }
207}