use super::{TemplatePolicy, TemplateSources};
use crate::services::lifecycle::site::test_support::TestResult;
use serde_json::json;
use std::os::unix::fs::PermissionsExt;
fn policy(root: &std::path::Path) -> Result<TemplatePolicy, serde_json::Error> {
serde_json::from_value(json!({"roots":[root],"command_bytes":1_048_576,"records":8,
"arguments":32,"environment_names":16,"text_bytes":1024,"stop_timeout_ms":2000}))
}
#[test]
fn only_regular_executables_inside_canonical_roots_are_proposable() -> TestResult {
let root = tempfile::tempdir()?;
let outside = tempfile::tempdir()?;
let policy = policy(root.path())?;
let command = root.path().join("program");
std::fs::write(&command, b"approved command bytes")?;
let refused = policy
.digest("echo", &command)
.err()
.ok_or("non executable admitted")?;
assert!(refused.to_string().contains("has no execute bit"));
std::fs::set_permissions(&command, std::fs::Permissions::from_mode(0o700))?;
let (_, approved) = policy.digest("echo", &command)?;
assert_eq!(approved.len(), 64);
let refused = policy
.digest("echo", root.path())
.err()
.ok_or("directory admitted")?;
assert!(refused.to_string().contains("not a regular file"));
let foreign = outside.path().join("program");
std::fs::write(&foreign, b"foreign")?;
std::fs::set_permissions(&foreign, std::fs::Permissions::from_mode(0o700))?;
let link = root.path().join("escape");
std::os::unix::fs::symlink(&foreign, &link)?;
assert!(
policy
.digest("echo", &foreign)
.err()
.ok_or("foreign command admitted")?
.to_string()
.contains("outside [auth.lys.templates] roots")
);
assert!(policy.digest("echo", &link).is_err());
std::fs::write(&command, b"changed command bytes")?;
assert_ne!(policy.digest("echo", &command)?.1, approved);
Ok(())
}
#[test]
fn legacy_configuration_and_root_policy_have_unambiguous_toml_shapes() -> TestResult {
#[derive(serde::Deserialize)]
struct Config {
templates: TemplateSources,
}
let legacy: Config = toml::from_str(
r#"[[templates]]
name="echo"
command="/usr/bin/true"
subjects=["echo"]
stop_timeout_ms=1000
"#,
)?;
assert_eq!(legacy.templates.len(), 1);
assert!(legacy.templates.policy.is_none());
let root = tempfile::tempdir()?;
let table: Config = toml::from_str(&format!(
r#"[templates]
roots=["{}"]
command_bytes=1048576
records=8
arguments=16
environment_names=8
text_bytes=1024
stop_timeout_ms=2000
[[templates.configured]]
name="echo"
command="/usr/bin/true"
subjects=["echo"]
stop_timeout_ms=1000
"#,
root.path().display()
))?;
assert_eq!(table.templates.len(), 1);
assert!(table.templates.policy.is_some());
assert!(toml::from_str::<Config>("[templates]\nroots=[]").is_err());
Ok(())
}
#[test]
fn args_and_environment_names_never_appear_in_configuration_debug() {
let template = crate::config::types::ServiceTemplateConfig {
name: "echo".into(),
command: "/usr/bin/true".into(),
args: vec!["private argument".into()],
subjects: vec!["echo".into()],
env: vec!["PRIVATE_ENV".into()],
stop_timeout_ms: 1000,
};
let debug = format!("{template:?}");
assert!(!debug.contains("private argument"));
assert!(!debug.contains("PRIVATE_ENV"));
}
#[test]
fn approved_template_is_live_without_restart_and_changed_command_never_starts() -> TestResult {
use crate::services::lifecycle::ServiceState;
use crate::services::lifecycle::site::{
SiteChange,
test_support::{Fixture, spec},
};
let root = tempfile::tempdir()?;
let command = root.path().join("program");
std::fs::write(&command, b"#!/bin/sh\nexit 0\n")?;
std::fs::set_permissions(&command, std::fs::Permissions::from_mode(0o700))?;
let fixture = Fixture::with_policy(Some(policy(root.path())?))?;
fixture.execute("create", SiteChange::CreateSite(spec()))?;
fixture.execute(
"bind",
SiteChange::BindBuilder {
agent: "builder".into(),
},
)?;
let proposal = fixture.lifecycle.propose_template(
"echo",
&command,
vec!["exact argument".into()],
vec!["LANG".into()],
1000,
)?;
let approved = proposal.digest.clone();
fixture.execute("template", SiteChange::CreateTemplate(proposal))?;
let list = fixture.lifecycle.list(|_| true)?;
assert_eq!(list["templates"][0]["name"], "echo");
fixture.execute("complete", SiteChange::Complete)?;
assert_eq!(
fixture.lifecycle.record("echo")?.state,
ServiceState::Asleep
);
assert_eq!(
fixture
.issuer
.grants
.lock()
.map_err(|_| "fixture grants poisoned")?
.len(),
1
);
std::fs::write(&command, b"#!/bin/sh\nexit 7\n")?;
let refused = fixture
.lifecycle
.deploy("echo", "person-owner")
.err()
.ok_or("changed approved command started")?;
let words = refused.to_string();
assert!(words.contains("is not the file approved on 10"));
assert!(words.contains(&format!("digest {approved} !=")));
assert_eq!(
fixture.lifecycle.list(|_| true)?["services"][0]["running"],
false
);
let registry = fixture.lifecycle.lock()?;
let template = registry
.construction
.templates
.get("echo")
.ok_or("live template missing")?;
assert!(template.transitions[0].receipt.is_some());
drop(registry);
Ok(())
}
#[test]
fn a_path_replacement_after_staging_cannot_change_the_executed_bytes() -> TestResult {
use crate::services::lifecycle::site::{
SiteChange,
test_support::{Fixture, spec},
};
let root = tempfile::tempdir()?;
let command = root.path().join("program");
let approved = b"#!/bin/sh\nexit 0\n";
std::fs::write(&command, approved)?;
std::fs::set_permissions(&command, std::fs::Permissions::from_mode(0o700))?;
let fixture = Fixture::with_policy(Some(policy(root.path())?))?;
fixture.execute("create", SiteChange::CreateSite(spec()))?;
let proposal = fixture
.lifecycle
.propose_template("echo", &command, vec![], vec![], 1000)?;
fixture.execute("template", SiteChange::CreateTemplate(proposal))?;
let template = fixture
.lifecycle
.lock()?
.construction
.templates
.get("echo")
.cloned()
.ok_or("template missing")?;
let staged = fixture.lifecycle.stage_template(&template)?;
let staged_path = staged.command().to_path_buf();
std::fs::remove_file(&command)?;
std::fs::write(&command, b"#!/bin/sh\nexit 7\n")?;
assert_eq!(std::fs::read(&staged_path)?, approved);
assert_eq!(
std::fs::metadata(&staged_path)?.permissions().mode() & 0o777,
0o500
);
drop(staged);
assert!(!staged_path.exists());
Ok(())
}