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 = "default_true")]
45 pub require_tls_identity: bool,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum HostPattern {
51 Exact(String),
53 Wildcard(String),
55 Any,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SecretInjection {
62 #[serde(default = "default_true")]
64 pub headers: bool,
65
66 #[serde(default = "default_true")]
68 pub basic_auth: bool,
69
70 #[serde(default)]
72 pub query_params: bool,
73
74 #[serde(default)]
76 pub body: bool,
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub enum ViolationAction {
82 Block,
84 #[default]
86 BlockAndLog,
87 BlockAndTerminate,
89}
90
91impl std::fmt::Debug for SecretEntry {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("SecretEntry")
98 .field("env_var", &self.env_var)
99 .field("value", &"[REDACTED]")
100 .field("placeholder", &self.placeholder)
101 .field("allowed_hosts", &self.allowed_hosts)
102 .field("injection", &self.injection)
103 .field("require_tls_identity", &self.require_tls_identity)
104 .finish()
105 }
106}
107
108impl HostPattern {
109 pub fn matches(&self, hostname: &str) -> bool {
114 match self {
115 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
116 HostPattern::Wildcard(pattern) => {
117 if let Some(suffix) = pattern.strip_prefix("*.") {
118 hostname.eq_ignore_ascii_case(suffix)
119 || (hostname.len() > suffix.len() + 1
120 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
121 && hostname[hostname.len() - suffix.len()..]
122 .eq_ignore_ascii_case(suffix))
123 } else {
124 hostname.eq_ignore_ascii_case(pattern)
125 }
126 }
127 HostPattern::Any => true,
128 }
129 }
130}
131
132impl Default for SecretInjection {
137 fn default() -> Self {
138 Self {
139 headers: true,
140 basic_auth: true,
141 query_params: false,
142 body: false,
143 }
144 }
145}
146
147fn default_true() -> bool {
152 true
153}
154
155#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn exact_host_match() {
165 let p = HostPattern::Exact("api.openai.com".into());
166 assert!(p.matches("api.openai.com"));
167 assert!(p.matches("API.OpenAI.com"));
168 assert!(!p.matches("evil.com"));
169 }
170
171 #[test]
172 fn wildcard_host_match() {
173 let p = HostPattern::Wildcard("*.openai.com".into());
174 assert!(p.matches("api.openai.com"));
175 assert!(p.matches("openai.com"));
176 assert!(!p.matches("evil.com"));
177 }
178
179 #[test]
180 fn any_host_match() {
181 let p = HostPattern::Any;
182 assert!(p.matches("anything.com"));
183 }
184
185 #[test]
186 fn default_injection_scopes() {
187 let inj = SecretInjection::default();
188 assert!(inj.headers);
189 assert!(inj.basic_auth);
190 assert!(!inj.query_params);
191 assert!(!inj.body);
192 }
193
194 #[test]
195 fn default_require_tls_identity() {
196 let entry = SecretEntry {
197 env_var: "K".into(),
198 value: "v".into(),
199 placeholder: "$K".into(),
200 allowed_hosts: vec![],
201 injection: SecretInjection::default(),
202 require_tls_identity: true,
203 };
204 assert!(entry.require_tls_identity);
205 }
206}