Implement profile inheritance

This commit is contained in:
2026-04-26 23:51:32 +02:00
parent 7f9b21ef4f
commit c77dbc10c3
5 changed files with 357 additions and 68 deletions
+50 -16
View File
@@ -289,9 +289,7 @@ pub struct FileConfig {
#[serde(flatten)]
pub options: Options,
#[serde(default)]
pub profile: HashMap<String, Options>,
#[serde(rename = "default-profile", default)]
pub default_profile: Option<String>,
pub profiles: HashMap<String, Options>,
#[serde(rename = "extra-config", default)]
pub extra_config: Option<PathBuf>,
// Collects unrecognized keys; deny_unknown_fields is incompatible with flatten.
@@ -339,18 +337,17 @@ impl FileConfig {
}
fn merge_with(self, extra: FileConfig) -> FileConfig {
let mut profile = self.profile;
for (profile_name, profile_options) in extra.profile {
let merged = match profile.remove(&profile_name) {
let mut profiles = self.profiles;
for (profile_name, profile_options) in extra.profiles {
let merged = match profiles.remove(&profile_name) {
Some(existing) => existing.merge_with(profile_options),
None => profile_options,
};
profile.insert(profile_name, merged);
profiles.insert(profile_name, merged);
}
FileConfig {
options: self.options.merge_with(extra.options),
profile,
default_profile: extra.default_profile.or(self.default_profile),
profiles,
extra_config: None,
_unknown: HashMap::new(),
}
@@ -367,21 +364,57 @@ impl FileConfig {
Ok(config)
}
fn resolve_profile(&self, name: Option<&str>) -> Result<Options, SandboxError> {
match name.or(self.default_profile.as_deref()) {
Some(n) => self
.profile
.get(n)
.cloned()
.ok_or_else(|| SandboxError::ProfileNotFound(n.to_string())),
fn resolve_profile(&self, selected: Option<&str>) -> Result<Options, SandboxError> {
match selected.or(self.options.profile.as_deref()) {
Some(leaf) => self.resolve_chain(leaf),
None => Ok(Options::default()),
}
}
fn resolve_chain(&self, leaf: &str) -> Result<Options, SandboxError> {
let chain = self.collect_chain(leaf)?;
let merged = chain
.into_iter()
.rev()
.fold(Options::default(), |base, child| base.merge_with(child));
Ok(merged)
}
fn collect_chain(&self, leaf: &str) -> Result<Vec<Options>, SandboxError> {
let mut visited: Vec<String> = Vec::new();
let mut chain: Vec<Options> = Vec::new();
let mut current = leaf.to_string();
loop {
if visited.iter().any(|seen| seen == &current) {
visited.push(current);
return Err(SandboxError::ProfileCycle(visited));
}
let options = self
.profiles
.get(&current)
.cloned()
.ok_or_else(|| SandboxError::ProfileNotFound(current.clone()))?;
visited.push(current);
let parent = options.profile.clone();
chain.push(options);
match parent {
Some(p) => current = p,
None => return Ok(chain),
}
}
}
}
#[derive(Deserialize, Default, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Options {
pub profile: Option<String>,
pub blacklist: Option<bool>,
pub whitelist: Option<bool>,
pub hardened: Option<bool>,
@@ -410,6 +443,7 @@ impl Options {
fn merge_with(self, extra: Options) -> Options {
let (blacklist, whitelist) = pick_mode_flags(&self, &extra);
Options {
profile: extra.profile.or(self.profile),
blacklist,
whitelist,
hardened: extra.hardened.or(self.hardened),
+8
View File
@@ -21,6 +21,7 @@ pub enum SandboxError {
source: toml::de::Error,
},
ProfileNotFound(String),
ProfileCycle(Vec<String>),
ConflictingMode,
ConflictingEnvKey(String),
InvalidEnvEntry(String),
@@ -65,6 +66,13 @@ impl std::fmt::Display for SandboxError {
write!(f, "cannot parse config file '{}': {source}", path.display())
}
Self::ProfileNotFound(name) => write!(f, "profile not found in config: {name}"),
Self::ProfileCycle(chain) => {
write!(
f,
"profile inheritance cycle detected: {}",
chain.join(" -> ")
)
}
Self::ConflictingMode => write!(
f,
"config section sets both blacklist and whitelist to true"