Replace setenv with env list supporting host passthrough
This commit is contained in:
+135
-31
@@ -1,11 +1,11 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::cli::Args;
|
||||
use crate::{SandboxConfig, SandboxError, SandboxMode};
|
||||
use crate::{EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
||||
|
||||
pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfig, SandboxError> {
|
||||
let (mut globals, mut profile) = match file_config {
|
||||
@@ -26,6 +26,14 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
||||
let command = resolve_binary(&command)
|
||||
.ok_or_else(|| SandboxError::CommandNotFound(PathBuf::from(&command)))?;
|
||||
|
||||
let env = dedupe_env_last_wins(parse_env_entries(&merge_vecs(
|
||||
args.env,
|
||||
&profile.env,
|
||||
&globals.env,
|
||||
))?);
|
||||
let unsetenv = merge_vecs(args.unsetenv, &profile.unsetenv, &globals.unsetenv);
|
||||
reject_env_key_conflicts(&env, &unsetenv)?;
|
||||
|
||||
Ok(SandboxConfig {
|
||||
mode: merge_mode(args.blacklist, args.whitelist, &profile, &globals),
|
||||
hardened: merge_flag(
|
||||
@@ -59,8 +67,8 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
||||
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
||||
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
||||
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
|
||||
setenv: merge_setenv(args.setenv, &profile.setenv, &globals.setenv),
|
||||
unsetenv: merge_vecs(args.unsetenv, &profile.unsetenv, &globals.unsetenv),
|
||||
env,
|
||||
unsetenv,
|
||||
bwrap_args: split_bwrap_args(merge_vecs(
|
||||
args.bwrap_args,
|
||||
&profile.bwrap_args,
|
||||
@@ -166,19 +174,36 @@ fn merge_vecs<T: Clone>(cli: Vec<T>, profile: &[T], globals: &[T]) -> Vec<T> {
|
||||
globals.iter().chain(profile).cloned().chain(cli).collect()
|
||||
}
|
||||
|
||||
fn merge_setenv(
|
||||
cli: Vec<(String, String)>,
|
||||
profile: &BTreeMap<String, String>,
|
||||
globals: &BTreeMap<String, String>,
|
||||
) -> Vec<(String, String)> {
|
||||
let mut merged: BTreeMap<String, String> = globals.clone();
|
||||
for (k, v) in profile {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
fn parse_env_entries(raw: &[String]) -> Result<Vec<EnvEntry>, SandboxError> {
|
||||
raw.iter()
|
||||
.map(|s| match s.split_once('=') {
|
||||
None if s.is_empty() => Err(SandboxError::InvalidEnvEntry(s.clone())),
|
||||
None => Ok(EnvEntry::Keep(s.clone())),
|
||||
Some(("", _)) => Err(SandboxError::InvalidEnvEntry(s.clone())),
|
||||
Some((key, value)) => Ok(EnvEntry::Set(key.to_string(), value.to_string())),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn dedupe_env_last_wins(entries: Vec<EnvEntry>) -> Vec<EnvEntry> {
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut result: Vec<EnvEntry> = Vec::new();
|
||||
for entry in entries.into_iter().rev() {
|
||||
if seen.insert(entry.key().to_string()) {
|
||||
result.push(entry);
|
||||
}
|
||||
}
|
||||
for (k, v) in cli {
|
||||
merged.insert(k, v);
|
||||
result.reverse();
|
||||
result
|
||||
}
|
||||
|
||||
fn reject_env_key_conflicts(env: &[EnvEntry], unsetenv: &[String]) -> Result<(), SandboxError> {
|
||||
let env_keys: HashSet<&str> = env.iter().map(EnvEntry::key).collect();
|
||||
let unsetenv_keys: HashSet<&str> = unsetenv.iter().map(String::as_str).collect();
|
||||
match env_keys.intersection(&unsetenv_keys).next() {
|
||||
Some(&key) => Err(SandboxError::ConflictingEnvKey(key.to_string())),
|
||||
None => Ok(()),
|
||||
}
|
||||
merged.into_iter().collect()
|
||||
}
|
||||
|
||||
fn resolve_command(
|
||||
@@ -295,7 +320,7 @@ pub struct Options {
|
||||
#[serde(default)]
|
||||
pub mask: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub setenv: BTreeMap<String, String>,
|
||||
pub env: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub unsetenv: Vec<String>,
|
||||
#[serde(default)]
|
||||
@@ -937,22 +962,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_setenv_merges_globals_profile_cli() {
|
||||
fn build_env_accumulates_disjoint_keys() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
setenv: BTreeMap::from([
|
||||
("A".into(), "global".into()),
|
||||
("B".into(), "global".into()),
|
||||
]),
|
||||
env: vec!["A=global".into(), "B".into()],
|
||||
..Options::default()
|
||||
},
|
||||
profile: HashMap::from([(
|
||||
"p".into(),
|
||||
Options {
|
||||
setenv: BTreeMap::from([
|
||||
("B".into(), "profile".into()),
|
||||
("C".into(), "profile".into()),
|
||||
]),
|
||||
env: vec!["C=profile".into()],
|
||||
..Options::default()
|
||||
},
|
||||
)]),
|
||||
@@ -960,21 +979,106 @@ mod tests {
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
setenv: vec![("C".into(), "cli".into()), ("D".into(), "cli".into())],
|
||||
env: vec!["D".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
assert_eq!(
|
||||
config.setenv,
|
||||
config.env,
|
||||
vec![
|
||||
("A".into(), "global".into()),
|
||||
("B".into(), "profile".into()),
|
||||
("C".into(), "cli".into()),
|
||||
("D".into(), "cli".into()),
|
||||
EnvEntry::Set("A".into(), "global".into()),
|
||||
EnvEntry::Keep("B".into()),
|
||||
EnvEntry::Set("C".into(), "profile".into()),
|
||||
EnvEntry::Keep("D".into()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_env_later_tier_overrides_earlier() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
env: vec!["A=global".into()],
|
||||
..Options::default()
|
||||
},
|
||||
profile: HashMap::from([(
|
||||
"p".into(),
|
||||
Options {
|
||||
env: vec!["A=profile".into()],
|
||||
..Options::default()
|
||||
},
|
||||
)]),
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
env: vec!["A=cli".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
assert_eq!(config.env, vec![EnvEntry::Set("A".into(), "cli".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_env_conflicts_with_unsetenv() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
env: vec!["X=1".into()],
|
||||
unsetenv: vec!["X".into()],
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
build(args_with_command(), Some(file_config)),
|
||||
Err(SandboxError::ConflictingEnvKey(k)) if k == "X"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_env_keep_conflicts_with_unsetenv() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
env: vec!["X".into()],
|
||||
unsetenv: vec!["X".into()],
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
build(args_with_command(), Some(file_config)),
|
||||
Err(SandboxError::ConflictingEnvKey(k)) if k == "X"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_env_rejects_empty_key() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
env: vec!["=foo".into()],
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
build(args_with_command(), Some(file_config)),
|
||||
Err(SandboxError::InvalidEnvEntry(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_env_set_to_empty_string_is_allowed() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
env: vec!["DEBUG=".into()],
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||
assert_eq!(config.env, vec![EnvEntry::Set("DEBUG".into(), "".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_unsetenv_accumulates() {
|
||||
let file_config = FileConfig {
|
||||
|
||||
Reference in New Issue
Block a user