microsandbox_network/secrets/
config.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct SecretsConfig {
12 #[serde(default)]
14 pub secrets: Vec<SecretEntry>,
15
16 #[serde(default)]
18 pub on_violation: ViolationAction,
19}
20
21#[derive(Clone, Serialize, Deserialize)]
23pub struct SecretEntry {
24 pub env_var: String,
26
27 pub value: String,
29
30 pub placeholder: String,
32
33 #[serde(default)]
35 pub allowed_hosts: Vec<HostPattern>,
36
37 #[serde(default)]
39 pub injection: SecretInjection,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub on_violation: Option<ViolationAction>,
44
45 #[serde(default = "default_true")]
49 pub require_tls_identity: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "kebab-case")]
55pub enum HostPattern {
56 #[serde(alias = "Exact")]
58 Exact(String),
59 #[serde(alias = "Wildcard")]
61 Wildcard(String),
62 #[serde(alias = "Any")]
64 Any,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct SecretInjection {
70 #[serde(default = "default_true")]
72 pub headers: bool,
73
74 #[serde(default = "default_true")]
76 pub basic_auth: bool,
77
78 #[serde(default)]
80 pub query_params: bool,
81
82 #[serde(default)]
84 pub body: bool,
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub enum ViolationAction {
91 #[serde(alias = "Block")]
93 Block,
94 #[default]
96 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
97 BlockAndLog,
98 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
100 BlockAndTerminate,
101 #[serde(alias = "Passthrough")]
103 Passthrough(Vec<HostPattern>),
104}
105
106impl std::fmt::Debug for SecretEntry {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("SecretEntry")
113 .field("env_var", &self.env_var)
114 .field("value", &"[REDACTED]")
115 .field("placeholder", &self.placeholder)
116 .field("allowed_hosts", &self.allowed_hosts)
117 .field("injection", &self.injection)
118 .field("on_violation", &self.on_violation)
119 .field("require_tls_identity", &self.require_tls_identity)
120 .finish()
121 }
122}
123
124impl HostPattern {
125 pub fn matches(&self, hostname: &str) -> bool {
130 match self {
131 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
132 HostPattern::Wildcard(pattern) => {
133 if let Some(suffix) = pattern.strip_prefix("*.") {
134 hostname.eq_ignore_ascii_case(suffix)
135 || (hostname.len() > suffix.len() + 1
136 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
137 && hostname[hostname.len() - suffix.len()..]
138 .eq_ignore_ascii_case(suffix))
139 } else {
140 hostname.eq_ignore_ascii_case(pattern)
141 }
142 }
143 HostPattern::Any => true,
144 }
145 }
146}
147
148impl Default for SecretInjection {
153 fn default() -> Self {
154 Self {
155 headers: true,
156 basic_auth: true,
157 query_params: false,
158 body: false,
159 }
160 }
161}
162
163fn default_true() -> bool {
168 true
169}
170
171#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn exact_host_match() {
181 let p = HostPattern::Exact("api.openai.com".into());
182 assert!(p.matches("api.openai.com"));
183 assert!(p.matches("API.OpenAI.com"));
184 assert!(!p.matches("evil.com"));
185 }
186
187 #[test]
188 fn wildcard_host_match() {
189 let p = HostPattern::Wildcard("*.openai.com".into());
190 assert!(p.matches("api.openai.com"));
191 assert!(p.matches("openai.com"));
192 assert!(!p.matches("evil.com"));
193 }
194
195 #[test]
196 fn any_host_match() {
197 let p = HostPattern::Any;
198 assert!(p.matches("anything.com"));
199 }
200
201 #[test]
202 fn default_injection_scopes() {
203 let inj = SecretInjection::default();
204 assert!(inj.headers);
205 assert!(inj.basic_auth);
206 assert!(!inj.query_params);
207 assert!(!inj.body);
208 }
209
210 #[test]
211 fn default_require_tls_identity() {
212 let entry = SecretEntry {
213 env_var: "K".into(),
214 value: "v".into(),
215 placeholder: "$K".into(),
216 allowed_hosts: vec![],
217 injection: SecretInjection::default(),
218 on_violation: None,
219 require_tls_identity: true,
220 };
221 assert!(entry.require_tls_identity);
222 }
223
224 #[test]
225 fn violation_action_serializes_with_sdk_casing() {
226 let action = ViolationAction::Passthrough(vec![
227 HostPattern::Exact("api.anthropic.com".into()),
228 HostPattern::Wildcard("*.anthropic.com".into()),
229 HostPattern::Any,
230 ]);
231
232 assert_eq!(
233 serde_json::to_string(&action).unwrap(),
234 r#"{"passthrough":[{"exact":"api.anthropic.com"},{"wildcard":"*.anthropic.com"},"any"]}"#
235 );
236 assert_eq!(
237 serde_json::to_string(&ViolationAction::BlockAndLog).unwrap(),
238 r#""block-and-log""#
239 );
240 assert_eq!(
241 serde_json::to_string(&ViolationAction::BlockAndTerminate).unwrap(),
242 r#""block-and-terminate""#
243 );
244 }
245
246 #[test]
247 fn violation_action_accepts_legacy_pascal_case() {
248 let action: ViolationAction =
249 serde_json::from_str(r#"{"Passthrough":[{"Exact":"api.anthropic.com"}]}"#).unwrap();
250
251 assert_eq!(
252 action,
253 ViolationAction::Passthrough(vec![HostPattern::Exact("api.anthropic.com".into())])
254 );
255 assert_eq!(
256 serde_json::from_str::<ViolationAction>(r#""BlockAndTerminate""#).unwrap(),
257 ViolationAction::BlockAndTerminate
258 );
259 }
260}