Implement profile inheritance
This commit is contained in:
+50
-16
@@ -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 == ¤t) {
|
||||
visited.push(current);
|
||||
return Err(SandboxError::ProfileCycle(visited));
|
||||
}
|
||||
|
||||
let options = self
|
||||
.profiles
|
||||
.get(¤t)
|
||||
.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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user