pub mod http_proxy;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretRef {
pub name: String,
pub inject_via: InjectionMethod,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum InjectionMethod {
EnvVar {
var_name: String,
},
File {
path: String,
mode: u32,
},
Stdin,
}
#[derive(Debug, Clone)]
pub struct FileInjection {
pub path: String,
pub content: String,
pub mode: u32,
}
pub struct CredentialProxy {
secrets: std::collections::HashMap<String, String>,
}
impl CredentialProxy {
pub fn new() -> Self {
Self {
secrets: std::collections::HashMap::new(),
}
}
pub fn register(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.secrets.insert(name.into(), value.into());
}
#[inline]
#[must_use]
pub fn resolve(&self, secret_ref: &SecretRef) -> Option<&str> {
self.secrets.get(&secret_ref.name).map(|s| s.as_str())
}
#[must_use]
pub fn env_vars(&self, refs: &[SecretRef]) -> Vec<(String, String)> {
refs.iter()
.filter_map(|r| {
let value = self.resolve(r)?;
match &r.inject_via {
InjectionMethod::EnvVar { var_name } => {
Some((var_name.clone(), value.to_owned()))
}
_ => None,
}
})
.collect()
}
#[must_use]
pub fn file_injections(&self, refs: &[SecretRef]) -> Vec<FileInjection> {
refs.iter()
.filter_map(|r| {
let value = self.resolve(r)?;
match &r.inject_via {
InjectionMethod::File { path, mode } => Some(FileInjection {
path: path.clone(),
content: value.to_owned(),
mode: *mode,
}),
_ => None,
}
})
.collect()
}
#[must_use]
pub fn stdin_payload(&self, refs: &[SecretRef]) -> Option<String> {
let parts: Vec<&str> = refs
.iter()
.filter_map(|r| {
if matches!(r.inject_via, InjectionMethod::Stdin) {
self.resolve(r)
} else {
None
}
})
.collect();
if parts.is_empty() {
None
} else {
Some(parts.join("\n"))
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.secrets.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.secrets.is_empty()
}
}
impl Default for CredentialProxy {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_and_resolve() {
let mut proxy = CredentialProxy::new();
proxy.register("API_KEY", "sk-12345");
let r = SecretRef {
name: "API_KEY".into(),
inject_via: InjectionMethod::EnvVar {
var_name: "OPENAI_API_KEY".into(),
},
};
assert_eq!(proxy.resolve(&r), Some("sk-12345"));
}
#[test]
fn resolve_missing() {
let proxy = CredentialProxy::new();
let r = SecretRef {
name: "NOPE".into(),
inject_via: InjectionMethod::Stdin,
};
assert!(proxy.resolve(&r).is_none());
}
#[test]
fn env_vars_generation() {
let mut proxy = CredentialProxy::new();
proxy.register("KEY1", "val1");
proxy.register("KEY2", "val2");
let refs = vec![
SecretRef {
name: "KEY1".into(),
inject_via: InjectionMethod::EnvVar {
var_name: "MY_KEY_1".into(),
},
},
SecretRef {
name: "KEY2".into(),
inject_via: InjectionMethod::File {
path: "/tmp/secret".into(),
mode: 0o600,
},
},
];
let vars = proxy.env_vars(&refs);
assert_eq!(vars.len(), 1); assert_eq!(vars[0], ("MY_KEY_1".into(), "val1".into()));
}
#[test]
fn empty_proxy() {
let proxy = CredentialProxy::new();
assert!(proxy.is_empty());
assert_eq!(proxy.len(), 0);
}
#[test]
fn file_injections() {
let mut proxy = CredentialProxy::new();
proxy.register("DB_CERT", "-----BEGIN CERTIFICATE-----\nMII...");
proxy.register("API_KEY", "sk-12345");
let refs = vec![
SecretRef {
name: "DB_CERT".into(),
inject_via: InjectionMethod::File {
path: "/etc/ssl/db.pem".into(),
mode: 0o600,
},
},
SecretRef {
name: "API_KEY".into(),
inject_via: InjectionMethod::EnvVar {
var_name: "KEY".into(),
},
},
];
let files = proxy.file_injections(&refs);
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "/etc/ssl/db.pem");
assert!(files[0].content.contains("CERTIFICATE"));
assert_eq!(files[0].mode, 0o600);
}
#[test]
fn file_injection_missing_secret() {
let proxy = CredentialProxy::new();
let refs = vec![SecretRef {
name: "MISSING".into(),
inject_via: InjectionMethod::File {
path: "/tmp/secret".into(),
mode: 0o400,
},
}];
assert!(proxy.file_injections(&refs).is_empty());
}
#[test]
fn stdin_payload() {
let mut proxy = CredentialProxy::new();
proxy.register("TOKEN_A", "secret-a");
proxy.register("TOKEN_B", "secret-b");
let refs = vec![
SecretRef {
name: "TOKEN_A".into(),
inject_via: InjectionMethod::Stdin,
},
SecretRef {
name: "TOKEN_B".into(),
inject_via: InjectionMethod::Stdin,
},
];
let payload = proxy.stdin_payload(&refs).unwrap();
assert_eq!(payload, "secret-a\nsecret-b");
}
#[test]
fn stdin_payload_none_when_empty() {
let proxy = CredentialProxy::new();
let refs = vec![SecretRef {
name: "KEY".into(),
inject_via: InjectionMethod::EnvVar {
var_name: "X".into(),
},
}];
assert!(proxy.stdin_payload(&refs).is_none());
}
#[test]
fn stdin_payload_missing_secret() {
let proxy = CredentialProxy::new();
let refs = vec![SecretRef {
name: "MISSING".into(),
inject_via: InjectionMethod::Stdin,
}];
assert!(proxy.stdin_payload(&refs).is_none());
}
#[test]
fn mixed_injection_methods() {
let mut proxy = CredentialProxy::new();
proxy.register("ENV_SECRET", "env-val");
proxy.register("FILE_SECRET", "file-val");
proxy.register("STDIN_SECRET", "stdin-val");
let refs = vec![
SecretRef {
name: "ENV_SECRET".into(),
inject_via: InjectionMethod::EnvVar {
var_name: "MY_ENV".into(),
},
},
SecretRef {
name: "FILE_SECRET".into(),
inject_via: InjectionMethod::File {
path: "/run/secrets/key".into(),
mode: 0o400,
},
},
SecretRef {
name: "STDIN_SECRET".into(),
inject_via: InjectionMethod::Stdin,
},
];
assert_eq!(proxy.env_vars(&refs).len(), 1);
assert_eq!(proxy.file_injections(&refs).len(), 1);
assert!(proxy.stdin_payload(&refs).is_some());
}
}