1pub mod http_proxy;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SecretRef {
15 pub name: String,
17 pub inject_via: InjectionMethod,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[non_exhaustive]
24pub enum InjectionMethod {
25 EnvVar {
27 var_name: String,
29 },
30 File {
32 path: String,
34 mode: u32,
36 },
37 Stdin,
39}
40
41#[derive(Debug, Clone)]
43pub struct FileInjection {
44 pub path: String,
46 pub content: String,
48 pub mode: u32,
50}
51
52pub struct CredentialProxy {
54 secrets: std::collections::HashMap<String, String>,
55}
56
57impl CredentialProxy {
58 pub fn new() -> Self {
60 Self {
61 secrets: std::collections::HashMap::new(),
62 }
63 }
64
65 pub fn register(&mut self, name: impl Into<String>, value: impl Into<String>) {
67 self.secrets.insert(name.into(), value.into());
68 }
69
70 #[inline]
72 #[must_use]
73 pub fn resolve(&self, secret_ref: &SecretRef) -> Option<&str> {
74 self.secrets.get(&secret_ref.name).map(|s| s.as_str())
75 }
76
77 #[must_use]
80 pub fn env_vars(&self, refs: &[SecretRef]) -> Vec<(String, String)> {
81 refs.iter()
82 .filter_map(|r| {
83 let value = self.resolve(r)?;
84 match &r.inject_via {
85 InjectionMethod::EnvVar { var_name } => {
86 Some((var_name.clone(), value.to_owned()))
87 }
88 _ => None,
89 }
90 })
91 .collect()
92 }
93
94 #[must_use]
98 pub fn file_injections(&self, refs: &[SecretRef]) -> Vec<FileInjection> {
99 refs.iter()
100 .filter_map(|r| {
101 let value = self.resolve(r)?;
102 match &r.inject_via {
103 InjectionMethod::File { path, mode } => Some(FileInjection {
104 path: path.clone(),
105 content: value.to_owned(),
106 mode: *mode,
107 }),
108 _ => None,
109 }
110 })
111 .collect()
112 }
113
114 #[must_use]
118 pub fn stdin_payload(&self, refs: &[SecretRef]) -> Option<String> {
119 let parts: Vec<&str> = refs
120 .iter()
121 .filter_map(|r| {
122 if matches!(r.inject_via, InjectionMethod::Stdin) {
123 self.resolve(r)
124 } else {
125 None
126 }
127 })
128 .collect();
129
130 if parts.is_empty() {
131 None
132 } else {
133 Some(parts.join("\n"))
134 }
135 }
136
137 #[inline]
139 #[must_use]
140 pub fn len(&self) -> usize {
141 self.secrets.len()
142 }
143
144 #[inline]
146 #[must_use]
147 pub fn is_empty(&self) -> bool {
148 self.secrets.is_empty()
149 }
150}
151
152impl Default for CredentialProxy {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn register_and_resolve() {
164 let mut proxy = CredentialProxy::new();
165 proxy.register("API_KEY", "sk-12345");
166 let r = SecretRef {
167 name: "API_KEY".into(),
168 inject_via: InjectionMethod::EnvVar {
169 var_name: "OPENAI_API_KEY".into(),
170 },
171 };
172 assert_eq!(proxy.resolve(&r), Some("sk-12345"));
173 }
174
175 #[test]
176 fn resolve_missing() {
177 let proxy = CredentialProxy::new();
178 let r = SecretRef {
179 name: "NOPE".into(),
180 inject_via: InjectionMethod::Stdin,
181 };
182 assert!(proxy.resolve(&r).is_none());
183 }
184
185 #[test]
186 fn env_vars_generation() {
187 let mut proxy = CredentialProxy::new();
188 proxy.register("KEY1", "val1");
189 proxy.register("KEY2", "val2");
190 let refs = vec![
191 SecretRef {
192 name: "KEY1".into(),
193 inject_via: InjectionMethod::EnvVar {
194 var_name: "MY_KEY_1".into(),
195 },
196 },
197 SecretRef {
198 name: "KEY2".into(),
199 inject_via: InjectionMethod::File {
200 path: "/tmp/secret".into(),
201 mode: 0o600,
202 },
203 },
204 ];
205 let vars = proxy.env_vars(&refs);
206 assert_eq!(vars.len(), 1); assert_eq!(vars[0], ("MY_KEY_1".into(), "val1".into()));
208 }
209
210 #[test]
211 fn empty_proxy() {
212 let proxy = CredentialProxy::new();
213 assert!(proxy.is_empty());
214 assert_eq!(proxy.len(), 0);
215 }
216
217 #[test]
218 fn file_injections() {
219 let mut proxy = CredentialProxy::new();
220 proxy.register("DB_CERT", "-----BEGIN CERTIFICATE-----\nMII...");
221 proxy.register("API_KEY", "sk-12345");
222
223 let refs = vec![
224 SecretRef {
225 name: "DB_CERT".into(),
226 inject_via: InjectionMethod::File {
227 path: "/etc/ssl/db.pem".into(),
228 mode: 0o600,
229 },
230 },
231 SecretRef {
232 name: "API_KEY".into(),
233 inject_via: InjectionMethod::EnvVar {
234 var_name: "KEY".into(),
235 },
236 },
237 ];
238
239 let files = proxy.file_injections(&refs);
240 assert_eq!(files.len(), 1);
241 assert_eq!(files[0].path, "/etc/ssl/db.pem");
242 assert!(files[0].content.contains("CERTIFICATE"));
243 assert_eq!(files[0].mode, 0o600);
244 }
245
246 #[test]
247 fn file_injection_missing_secret() {
248 let proxy = CredentialProxy::new();
249 let refs = vec![SecretRef {
250 name: "MISSING".into(),
251 inject_via: InjectionMethod::File {
252 path: "/tmp/secret".into(),
253 mode: 0o400,
254 },
255 }];
256 assert!(proxy.file_injections(&refs).is_empty());
257 }
258
259 #[test]
260 fn stdin_payload() {
261 let mut proxy = CredentialProxy::new();
262 proxy.register("TOKEN_A", "secret-a");
263 proxy.register("TOKEN_B", "secret-b");
264
265 let refs = vec![
266 SecretRef {
267 name: "TOKEN_A".into(),
268 inject_via: InjectionMethod::Stdin,
269 },
270 SecretRef {
271 name: "TOKEN_B".into(),
272 inject_via: InjectionMethod::Stdin,
273 },
274 ];
275
276 let payload = proxy.stdin_payload(&refs).unwrap();
277 assert_eq!(payload, "secret-a\nsecret-b");
278 }
279
280 #[test]
281 fn stdin_payload_none_when_empty() {
282 let proxy = CredentialProxy::new();
283 let refs = vec![SecretRef {
284 name: "KEY".into(),
285 inject_via: InjectionMethod::EnvVar {
286 var_name: "X".into(),
287 },
288 }];
289 assert!(proxy.stdin_payload(&refs).is_none());
290 }
291
292 #[test]
293 fn stdin_payload_missing_secret() {
294 let proxy = CredentialProxy::new();
295 let refs = vec![SecretRef {
296 name: "MISSING".into(),
297 inject_via: InjectionMethod::Stdin,
298 }];
299 assert!(proxy.stdin_payload(&refs).is_none());
300 }
301
302 #[test]
303 fn mixed_injection_methods() {
304 let mut proxy = CredentialProxy::new();
305 proxy.register("ENV_SECRET", "env-val");
306 proxy.register("FILE_SECRET", "file-val");
307 proxy.register("STDIN_SECRET", "stdin-val");
308
309 let refs = vec![
310 SecretRef {
311 name: "ENV_SECRET".into(),
312 inject_via: InjectionMethod::EnvVar {
313 var_name: "MY_ENV".into(),
314 },
315 },
316 SecretRef {
317 name: "FILE_SECRET".into(),
318 inject_via: InjectionMethod::File {
319 path: "/run/secrets/key".into(),
320 mode: 0o400,
321 },
322 },
323 SecretRef {
324 name: "STDIN_SECRET".into(),
325 inject_via: InjectionMethod::Stdin,
326 },
327 ];
328
329 assert_eq!(proxy.env_vars(&refs).len(), 1);
330 assert_eq!(proxy.file_injections(&refs).len(), 1);
331 assert!(proxy.stdin_payload(&refs).is_some());
332 }
333}