Reject unknown config keys
This commit is contained in:
@@ -145,6 +145,9 @@ pub struct FileConfig {
|
|||||||
pub options: Options,
|
pub options: Options,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub profile: HashMap<String, Options>,
|
pub profile: HashMap<String, Options>,
|
||||||
|
// Collects unrecognized keys; deny_unknown_fields is incompatible with flatten.
|
||||||
|
#[serde(flatten)]
|
||||||
|
_unknown: HashMap<String, toml::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileConfig {
|
impl FileConfig {
|
||||||
@@ -153,12 +156,26 @@ impl FileConfig {
|
|||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
source: e,
|
source: e,
|
||||||
})?;
|
})?;
|
||||||
toml::from_str(&contents).map_err(|e| SandboxError::ConfigParse {
|
Self::parse(&contents).map_err(|e| match e {
|
||||||
path: path.to_path_buf(),
|
SandboxError::ConfigParse { source, .. } => SandboxError::ConfigParse {
|
||||||
source: e,
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
other => other,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse(contents: &str) -> Result<Self, SandboxError> {
|
||||||
|
let config: Self = toml::from_str(contents).map_err(|e| SandboxError::ConfigParse {
|
||||||
|
path: PathBuf::new(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
if let Some(key) = config._unknown.keys().next() {
|
||||||
|
return Err(SandboxError::UnknownConfigKey(key.clone()));
|
||||||
|
}
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_profile(&self, name: Option<&str>) -> Result<Options, SandboxError> {
|
fn resolve_profile(&self, name: Option<&str>) -> Result<Options, SandboxError> {
|
||||||
match name {
|
match name {
|
||||||
Some(n) => self
|
Some(n) => self
|
||||||
@@ -172,7 +189,7 @@ impl FileConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone)]
|
#[derive(Deserialize, Default, Clone)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
pub blacklist: Option<bool>,
|
pub blacklist: Option<bool>,
|
||||||
pub whitelist: Option<bool>,
|
pub whitelist: Option<bool>,
|
||||||
@@ -396,6 +413,7 @@ mod tests {
|
|||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
|
..FileConfig::default()
|
||||||
};
|
};
|
||||||
let args = Args {
|
let args = Args {
|
||||||
profile: Some("relaxed".into()),
|
profile: Some("relaxed".into()),
|
||||||
@@ -461,6 +479,7 @@ mod tests {
|
|||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
|
..FileConfig::default()
|
||||||
};
|
};
|
||||||
let args = Args {
|
let args = Args {
|
||||||
profile: Some("extra".into()),
|
profile: Some("extra".into()),
|
||||||
@@ -603,6 +622,7 @@ mod tests {
|
|||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
|
..FileConfig::default()
|
||||||
};
|
};
|
||||||
let args = Args {
|
let args = Args {
|
||||||
profile: Some("extra".into()),
|
profile: Some("extra".into()),
|
||||||
@@ -613,6 +633,31 @@ mod tests {
|
|||||||
assert_eq!(config.mask.len(), 3);
|
assert_eq!(config.mask.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_option_rejected() {
|
||||||
|
let toml = r#"
|
||||||
|
hardened = true
|
||||||
|
bogus = "nope"
|
||||||
|
"#;
|
||||||
|
assert!(matches!(
|
||||||
|
FileConfig::parse(toml),
|
||||||
|
Err(SandboxError::UnknownConfigKey(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_profile_option_rejected() {
|
||||||
|
let toml = r#"
|
||||||
|
[profile.test]
|
||||||
|
hardened = true
|
||||||
|
frobnicate = 42
|
||||||
|
"#;
|
||||||
|
assert!(matches!(
|
||||||
|
FileConfig::parse(toml),
|
||||||
|
Err(SandboxError::ConfigParse { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
|
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
|
||||||
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
|
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
|
||||||
assert_eq!(actual, &expected);
|
assert_eq!(actual, &expected);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub enum SandboxError {
|
|||||||
},
|
},
|
||||||
ProfileNotFound(String),
|
ProfileNotFound(String),
|
||||||
ConflictingMode,
|
ConflictingMode,
|
||||||
|
UnknownConfigKey(String),
|
||||||
ConfigPathNotAbsolute(PathBuf),
|
ConfigPathNotAbsolute(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ impl std::fmt::Display for SandboxError {
|
|||||||
f,
|
f,
|
||||||
"config section sets both blacklist and whitelist to true"
|
"config section sets both blacklist and whitelist to true"
|
||||||
),
|
),
|
||||||
|
Self::UnknownConfigKey(key) => write!(f, "unknown config key: {key}"),
|
||||||
Self::ConfigPathNotAbsolute(p) => {
|
Self::ConfigPathNotAbsolute(p) => {
|
||||||
write!(f, "config path is not absolute: {}", p.display())
|
write!(f, "config path is not absolute: {}", p.display())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -578,6 +578,24 @@ fn config_invalid_toml_errors() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_unknown_key_errors() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let cfg = write_config(&dir, "hardened = true\nbogus = \"nope\"\n");
|
||||||
|
|
||||||
|
let output = sandbox(&["--config", &cfg])
|
||||||
|
.args(["--", "true"])
|
||||||
|
.output()
|
||||||
|
.expect("failed to execute");
|
||||||
|
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("unknown config key"),
|
||||||
|
"expected unknown key error, got: {stderr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mask_hides_directory() {
|
fn mask_hides_directory() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user