use regex::Regex;
fn debug_masked(f: &mut std::fmt::Formatter<'_>, name: &str, key_field: &str, key_val: &str) -> std::fmt::Result {
f.debug_struct(name).field(key_field, &key_val).field("value", &"[REDACTED]").finish()
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SecretMapping {
pub var: String,
pub value: String,
}
impl std::fmt::Debug for SecretMapping {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
debug_masked(f, "SecretMapping", "var", &self.var)
}
}
impl SecretMapping {
pub fn new(var: impl Into<String>, value: impl Into<String>) -> Self {
Self {
var: var.into(),
value: value.into(),
}
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct StringMapping {
pub token: String,
pub value: String,
}
impl std::fmt::Debug for StringMapping {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
debug_masked(f, "StringMapping", "token", &self.token)
}
}
impl StringMapping {
pub fn new(token: impl Into<String>, value: impl Into<String>) -> Self {
Self {
token: token.into(),
value: value.into(),
}
}
}
#[derive(Clone, Debug)]
pub enum HostPattern {
Exact(String),
Regex { pattern: String, re: Regex },
}
impl HostPattern {
pub fn exact(host: impl Into<String>) -> Self {
Self::Exact(host.into())
}
pub fn regex(pattern: &str) -> Result<Self, regex::Error> {
let re = Regex::new(pattern)?;
Ok(Self::Regex {
pattern: pattern.to_string(),
re,
})
}
pub fn matches(&self, host: &str) -> bool {
match self {
HostPattern::Exact(s) => s == host,
HostPattern::Regex { re, .. } => re.is_match(host),
}
}
}
#[derive(Clone, Debug)]
pub struct ConnectionPolicy {
pub pattern: HostPattern,
pub allow: bool,
}
impl ConnectionPolicy {
pub fn allow(pattern: HostPattern) -> Self {
Self {
pattern,
allow: true,
}
}
pub fn deny(pattern: HostPattern) -> Self {
Self {
pattern,
allow: false,
}
}
}
#[derive(Clone, Debug)]
pub struct SandboxConfig {
pub secrets: Vec<SecretMapping>,
pub strings: Vec<StringMapping>,
pub connections: Vec<ConnectionPolicy>,
pub allow_private_connect: bool,
pub upstream_ca: Option<std::path::PathBuf>,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
secrets: Vec::new(),
strings: Vec::new(),
connections: Vec::new(),
allow_private_connect: false,
upstream_ca: None,
}
}
}
#[derive(Clone, Debug)]
pub struct Sandbox {
pub(crate) config: SandboxConfig,
}
impl Sandbox {
pub fn new(config: SandboxConfig) -> Self {
Self { config }
}
pub async fn run(
&self,
program: &str,
args: &[String],
) -> Result<std::process::ExitStatus, Box<dyn std::error::Error + Send + Sync>> {
crate::proxy::run_impl(
program,
args,
self.config.secrets.clone(),
self.config.strings.clone(),
self.config.allow_private_connect,
self.config.upstream_ca.clone(),
self.config.connections.clone(),
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_mapping_creation() {
let m = SecretMapping {
var: "API_KEY".to_string(),
value: "secret123".to_string(),
};
assert_eq!(m.var, "API_KEY");
assert_eq!(m.value, "secret123");
}
#[test]
fn string_mapping_creation() {
let m = StringMapping {
token: "__API_KEY__".to_string(),
value: "secret123".to_string(),
};
assert_eq!(m.token, "__API_KEY__");
assert_eq!(m.value, "secret123");
}
}