Pin the rule that a binding sets only names its recipe declares
The secret-files campaign rests on one admit line: a binding may bind only names its recipe declares, so a recipe that declares a secret only as its *_FILE name leaves a plaintext value no place in a binding. Its only test, extra_operator_environment_is_rejected, asserted is_err(), which any other refusal satisfies. The replacement tests assert this refusal exactly, each against a declared twin that admits: - an undeclared [workload.environment] name; - an undeclared [workload.secret_files] name; - the plaintext twin of a declared *_FILE name, the shape of the Heimdall leak; - `idunn validate` returns the refusal, as main prints it, without the bound value (a canary), pinning the admit layer of P6. Tests only. The deployment test fixtures become pub(crate) so control_plane's validate test can reuse them. Idunn secret-files cut, Cut 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
08d346dfae
commit
d21ead285b
2 changed files with 101 additions and 10 deletions
|
|
@ -10216,6 +10216,30 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_refuses_an_undeclared_binding_name_without_echoing_its_value() {
|
||||
use crate::deployment::tests::{
|
||||
UNDECLARED_INPUT_REFUSAL, binding_with_environment, recipe_declaring_inputs,
|
||||
};
|
||||
|
||||
let canary = "canary-bound-value-4e91c7";
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let recipe = directory.path().join("recipe.toml");
|
||||
fs::write(&recipe, recipe_declaring_inputs()).unwrap();
|
||||
let declared = directory.path().join("declared.toml");
|
||||
fs::write(&declared, binding_with_environment("DECLARED_INPUT", canary)).unwrap();
|
||||
let undeclared = directory.path().join("undeclared.toml");
|
||||
fs::write(&undeclared, binding_with_environment("UNDECLARED_INPUT", canary)).unwrap();
|
||||
|
||||
validate(&recipe, Some(&declared)).unwrap();
|
||||
|
||||
// `idunn` returns this error from main, which prints its Debug form,
|
||||
// context chain included, and exits non-zero.
|
||||
let printed = format!("{:?}", validate(&recipe, Some(&undeclared)).unwrap_err());
|
||||
assert!(printed.contains(UNDECLARED_INPUT_REFUSAL), "{printed}");
|
||||
assert!(!printed.contains("canary"), "validate echoed a bound value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_exposes_only_declarative_commands() {
|
||||
for arguments in [
|
||||
|
|
|
|||
|
|
@ -1928,10 +1928,11 @@ fn require_host(value: &str) -> Result<()> {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
const RECIPE: &str = r#"
|
||||
// `idunn validate`'s tests in control_plane reuse these two fixtures.
|
||||
pub(crate) const RECIPE: &str = r#"
|
||||
schema = "gamecult.idunn.target_declaration.v1"
|
||||
target = "test-service"
|
||||
source_stamp_environment = "TEST_SERVICE_BUILD_COMMIT"
|
||||
|
|
@ -2003,7 +2004,7 @@ compatibility = "v1"
|
|||
startup = "before-promotion"
|
||||
"#;
|
||||
|
||||
const BINDING: &str = r#"
|
||||
pub(crate) const BINDING: &str = r#"
|
||||
schema = "gamecult.idunn.operator_binding.v2"
|
||||
target = "test-service"
|
||||
profiles = ["aetheria", "full-gamecult"]
|
||||
|
|
@ -2175,15 +2176,81 @@ nodes = ["yggdrasil"]
|
|||
assert!(TargetDeclaration::parse(&input).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_operator_environment_is_rejected() {
|
||||
let recipe = TargetDeclaration::parse(RECIPE).unwrap();
|
||||
let input = BINDING.replace(
|
||||
/// The refusal of a binding that sets a name its recipe does not declare.
|
||||
/// A recipe declares a secret only as a `*_FILE` name, so this one rule is
|
||||
/// what leaves a plaintext secret no place in a binding.
|
||||
pub(crate) const UNDECLARED_INPUT_REFUSAL: &str =
|
||||
"operator service environment contains an undeclared launch input";
|
||||
|
||||
/// RECIPE plus one declared plain input and one declared secret-file input.
|
||||
pub(crate) fn recipe_declaring_inputs() -> String {
|
||||
RECIPE.replace(
|
||||
"required_environment = [",
|
||||
"optional_environment = [\"DECLARED_INPUT\", \"DECLARED_SECRET_FILE\"]\nrequired_environment = [",
|
||||
)
|
||||
}
|
||||
|
||||
/// BINDING plus one `[workload.environment]` entry.
|
||||
pub(crate) fn binding_with_environment(name: &str, value: &str) -> String {
|
||||
BINDING.replace(
|
||||
"[workload.argument_bindings]",
|
||||
"[workload.environment]\nUNDECLARED = \"no\"\n\n[workload.argument_bindings]",
|
||||
&format!("[workload.environment]\n{name} = \"{value}\"\n\n[workload.argument_bindings]"),
|
||||
)
|
||||
}
|
||||
|
||||
/// BINDING plus one `[workload.secret_files]` entry.
|
||||
fn binding_with_secret_file(name: &str) -> String {
|
||||
BINDING.replace(
|
||||
"[workload.secret_files]\n",
|
||||
&format!(
|
||||
"[workload.secret_files]\n{name} = \"/etc/gamecult/test-service/credentials/{name}\"\n"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn admit_refusal(recipe: &TargetDeclaration, binding: &str) -> String {
|
||||
let binding = OperatorBinding::parse(binding).unwrap();
|
||||
format!("{:#}", binding.admit(recipe).unwrap_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binding_environment_sets_only_declared_names() {
|
||||
let recipe = TargetDeclaration::parse(&recipe_declaring_inputs()).unwrap();
|
||||
OperatorBinding::parse(&binding_with_environment("DECLARED_INPUT", "plain"))
|
||||
.unwrap()
|
||||
.admit(&recipe)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
admit_refusal(&recipe, &binding_with_environment("UNDECLARED_INPUT", "plain")),
|
||||
UNDECLARED_INPUT_REFUSAL
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binding_secret_files_set_only_declared_names() {
|
||||
let recipe = TargetDeclaration::parse(&recipe_declaring_inputs()).unwrap();
|
||||
OperatorBinding::parse(&binding_with_secret_file("DECLARED_SECRET_FILE"))
|
||||
.unwrap()
|
||||
.admit(&recipe)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
admit_refusal(&recipe, &binding_with_secret_file("UNDECLARED_SECRET_FILE")),
|
||||
UNDECLARED_INPUT_REFUSAL
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declaring_a_secret_file_does_not_admit_its_plaintext_twin() {
|
||||
// The shape of the Heimdall leak: the recipe declares only
|
||||
// DECLARED_SECRET_FILE, and a binding puts the value itself under
|
||||
// DECLARED_SECRET in plain environment.
|
||||
let recipe = TargetDeclaration::parse(&recipe_declaring_inputs()).unwrap();
|
||||
assert_eq!(
|
||||
admit_refusal(&recipe, &binding_with_environment("DECLARED_SECRET", "plain")),
|
||||
UNDECLARED_INPUT_REFUSAL
|
||||
);
|
||||
let binding = OperatorBinding::parse(&input).unwrap();
|
||||
assert!(binding.admit(&recipe).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue