microsandbox_network/secrets/
handle.rs1use std::sync::{Arc, RwLock};
9
10use super::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 super::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::super::config::{SecretEntry, SecretInjection};
124 use super::*;
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 injection: SecretInjection::default(),
135 on_violation: None,
136 require_tls_identity: true,
137 }],
138 ..SecretsConfig::default()
139 }
140 }
141
142 #[test]
143 fn rotate_swaps_value_for_future_loads() {
144 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
145 let before = handle.load();
146
147 handle.rotate_value("API_KEY", "new".into()).unwrap();
148
149 assert_eq!(before.secrets[0].value.as_str(), "old");
151 assert_eq!(handle.load().secrets[0].value.as_str(), "new");
152 assert_eq!(handle.load().secrets[0].placeholder, "$MSB_API_KEY");
153 }
154
155 #[test]
156 fn rotate_unknown_secret_is_an_error() {
157 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
158
159 assert_eq!(
160 handle.rotate_value("MISSING", "new".into()),
161 Err(SecretsUpdateError::UnknownSecret {
162 name: "MISSING".into()
163 })
164 );
165 }
166
167 #[test]
168 fn remove_drops_entry_and_is_idempotent() {
169 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
170
171 handle.remove("API_KEY");
172 handle.remove("API_KEY");
173
174 assert!(handle.load().secrets.is_empty());
175 }
176
177 #[test]
178 fn set_allowed_hosts_replaces_patterns() {
179 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
180
181 handle
182 .set_allowed_hosts("API_KEY", &["*.example.org".into(), "one.test".into()])
183 .unwrap();
184
185 assert_eq!(
186 handle.load().secrets[0].allowed_hosts,
187 vec![
188 HostPattern::Wildcard("*.example.org".into()),
189 HostPattern::Exact("one.test".into()),
190 ]
191 );
192 }
193
194 #[test]
195 fn set_allowed_hosts_rejects_empty_list() {
196 let handle = SecretsHandle::new(config_with_secret("API_KEY", "old"));
197
198 assert_eq!(
199 handle.set_allowed_hosts("API_KEY", &[]),
200 Err(SecretsUpdateError::MissingAllowedHosts {
201 name: "API_KEY".into()
202 })
203 );
204 assert_eq!(handle.load().secrets[0].allowed_hosts.len(), 1);
205 }
206}