Steward-owned second deployment stop named by the binding #5

Merged
eureka merged 3 commits from eureka-self/brake-steward-store into main 2026-11-13 18:11:46 +00:00
6 changed files with 471 additions and 41 deletions

View file

@ -318,6 +318,57 @@ runtime's record, another owner, an unsearchable or non-directory parent) reads
as unreadable and fails closed; the wait note carries the
`idunn-provision deployment-brake-clear` command that replaces it.
A binding may name a second stop, the target's steward:
```toml
[brakes.steward]
user = "mind-streampixels" # a login, never root
store = "/var/lib/gamecult/idunn-steward/streampixels/deployment-brake.cc"
```
Deploys admit only when every stop admits. The steward's store is read as owned
by that user's uid: the owner uid proves who wrote the file, and the wait note
names the stop by the binding's steward user, never by the record's `by` (which
is the verb's own text). Idunn never writes the steward directory and no key,
group or ACL is involved. The operator's store is a separate stop the steward
cannot clear, and the wait note names each holding stop.
The steward owns its directory, so it could unlink any file in it. Binding
validation therefore refuses a steward store whose directory is shared with,
inside or around the directory of any other Idunn authority store (deployment
and lifecycle brakes, trust anchor, write-lease record, release authority).
Validation cannot see the file system: the root who provisions the directory
makes it contain the steward's store and nothing else.
A steward store owned by another uid, or a steward user that does not resolve
to a non-root uid, is unreadable and fails closed. Only the steward (the
owner uid) can repair it: a clear run as root leaves a root-owned file that is
still unreadable.
### Provisioning a steward (root, once per target)
1. Create the store directory, owned by the steward and holding nothing else:
`install -d -o mind-streampixels -g mind-streampixels -m 0755
/var/lib/gamecult/idunn-steward/streampixels`. It must not be, contain or sit
inside any other authority directory, and no workload `ReadWritePaths` may
cover it. Idunn (root) reads it without further grants; no group or ACL.
2. Add the stanza above to the target's operator binding, after `[brakes]`,
with the steward's real login. Deploys planned afterwards see it.
3. The steward engages and clears as itself, never through root:
`idunn-provision deployment-brake-engage --store <store> --runtime-id <id>
--reason <text>` and `deployment-brake-clear` with the same flags.
4. The operator reads it with `idunn-provision deployment-brake-status --store
<store> --runtime-id <id> --owner-user mind-streampixels`.
The binding is sealed into each plan. A deploy already planned keeps the
binding it was sealed with: editing the stanza does not apply to it. If the
steward's login is deleted or its directory is lost, that deploy stays
unreadable-held at Sealing (or the static flip intent). `idunn cancel` withdraws
only a deploy held for a readiness disagreement before Fencing, and `idunn
expire` only one in Warming through Committing, so neither frees a deploy held
by the stop. Recovery is to restore the steward's login or recreate the
directory owned by it (and have the steward clear its store).
Record v6 (`deployment_admitted_at_unix_millis`, key 38) is forward-only: an
Idunn that predates v6 cannot read a migrated `control.cc`, so, as for v5 (see
`route-continuity-cut.md`, "An older Idunn refuses the v5 store"), take a

View file

@ -11,6 +11,8 @@
use std::path::Path;
use crate::deployment::BrakeBinding;
use anyhow::{Context, Result, anyhow, bail, ensure};
use cultcache_rs::{CultCacheEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore};
use serde::{Deserialize, Serialize};
@ -128,6 +130,72 @@ impl Stop {
}
}
/// One stop a binding names, with how to name it in a wait note.
pub(crate) struct NamedStop {
pub(crate) label: String,
/// The login the file system proves wrote the store: the steward user, whose
/// uid owns the file. None for the operator's store, whose record carries
/// only the verb's own claim of `by`.
pub(crate) proven_writer: Option<String>,
pub(crate) stop: Stop,
}
impl NamedStop {
/// The wait-note line for a stop that does not admit; None when it admits.
/// A steward stop is named by the proven owner, never by the record's `by`.
pub(crate) fn note(&self) -> Option<String> {
let label = &self.label;
match &self.stop {
Stop::Absent | Stop::Clear(_) => None,
Stop::Engaged(held) => Some(match &self.proven_writer {
Some(_) => format!("{label} engaged: {}", held.reason),
None => format!("{label} engaged by {}: {}", held.by, held.reason),
}),
Stop::Unreadable(why) => Some(format!("{label} is unreadable and fails closed: {why}")),
}
}
}
/// Every stop a binding names, each with the words that identify its store in
/// a wait note: the operator's, then the steward's when the binding names one.
/// Deployments admit iff every one admits. The operator's store is read as
/// owned by this process's uid; the steward's as owned by the steward user's
/// uid, so the file system proves who wrote it. A steward that does not
/// resolve to a non-root uid is an unreadable stop, never an absent one.
pub(crate) fn read_stops(brakes: &BrakeBinding, runtime_id: &str) -> Vec<NamedStop> {
let mut stops = vec![NamedStop {
label: "deployment brake".to_string(),
proven_writer: None,
stop: read_stop(&brakes.deployment_store, runtime_id, process_uid()),
}];
if let Some(steward) = &brakes.steward {
let label = format!("steward {} deployment brake", steward.user);
let stop = match crate::steward_orders::user_uid(&steward.user) {
Ok(uid) => read_stop_with_remedy(&steward.store, runtime_id, uid, |path| {
// Only the owner uid can mend it: a clear run as root leaves a
// root-owned file, which this reader refuses again.
format!(
"only {} (the store's owner uid) can repair it, by running as that user: \
idunn-provision deployment-brake-clear --store {} --runtime-id {runtime_id} \
--reason <text>; a clear run as root stays unreadable",
steward.user,
path.display()
)
}),
Err(why) => Stop::Unreadable(format!(
"{why:#}; the binding's steward user {} must be a non-root login",
steward.user
)),
};
stops.push(NamedStop {
label,
proven_writer: Some(steward.user.clone()),
stop,
});
}
stops
}
#[cfg(unix)]
pub(crate) fn process_uid() -> u32 {
// SAFETY: geteuid has no failure mode and no arguments.
@ -140,7 +208,7 @@ pub(crate) fn process_uid() -> u32 {
}
#[cfg(unix)]
fn login_name(uid: u32) -> Result<String> {
pub(crate) fn login_name(uid: u32) -> Result<String> {
// SAFETY: getpwuid returns null or a pointer into libc's static buffer,
// copied before any other call; the verbs are single threaded.
unsafe {
@ -153,7 +221,7 @@ fn login_name(uid: u32) -> Result<String> {
}
#[cfg(not(unix))]
fn login_name(_: u32) -> Result<String> {
pub(crate) fn login_name(_: u32) -> Result<String> {
std::env::var("USERNAME").context("USERNAME is unset")
}
@ -237,15 +305,26 @@ fn read_checked(path: &Path, runtime_id: &str, owner: u32) -> Result<Option<Depl
}
pub(crate) fn read_stop(path: &Path, runtime_id: &str, expected_owner_uid: u32) -> Stop {
read_stop_with_remedy(path, runtime_id, expected_owner_uid, |path| {
format!(
"to replace it run: idunn-provision deployment-brake-clear \
--store {} --runtime-id {runtime_id} --reason <text> (or deployment-brake-engage)",
path.display()
)
})
}
fn read_stop_with_remedy(
path: &Path,
runtime_id: &str,
expected_owner_uid: u32,
remedy: impl FnOnce(&Path) -> String,
) -> Stop {
match read_checked(path, runtime_id, expected_owner_uid) {
Ok(None) => Stop::Absent,
Ok(Some(record)) if record.state == StopState::Clear => Stop::Clear(record),
Ok(Some(record)) => Stop::Engaged(record),
Err(error) => Stop::Unreadable(format!(
"{error:#}; to replace it run: idunn-provision deployment-brake-clear \
--store {} --runtime-id {runtime_id} --reason <text> (or deployment-brake-engage)",
path.display()
)),
Err(error) => Stop::Unreadable(format!("{error:#}; {}", remedy(path))),
}
}
@ -383,6 +462,115 @@ mod tests {
Ok(())
}
/// A binding whose steward is the user running the test, so the steward's
/// store can really be owned by the steward. None when run as root, which
/// no steward may be.
fn binding_with_steward(dir: &Path, user: &str) -> BrakeBinding {
BrakeBinding {
deployment_store: dir.join("operator.cc"),
lifecycle_store: dir.join("lifecycle.cc"),
steward: Some(crate::deployment::StewardBrakeBinding {
user: user.into(),
store: dir.join("steward.cc"),
}),
}
}
fn admits_all(stops: &[NamedStop]) -> bool {
stops.iter().all(|named| named.stop.admits())
}
#[test]
fn the_steward_store_is_a_second_stop_and_the_operator_store_is_one_it_cannot_clear() -> Result<()> {
let me = process_uid();
if me == 0 {
eprintln!("SKIPPED: a steward is never root, and this run is root");
return Ok(());
}
let dir = tempfile::tempdir()?;
let binding = binding_with_steward(dir.path(), &login_name(me)?);
let (operator, steward) = (&binding.deployment_store, &binding.steward.as_ref().unwrap().store);
// Both absent: admits.
assert!(admits_all(&read_stops(&binding, "runtime")));
// Steward engaged alone: halts, and the stop names the steward's store.
replace_stop(steward, "runtime", StopState::Engaged, "steward freeze", 10)?;
let stops = read_stops(&binding, "runtime");
assert!(!admits_all(&stops));
assert!(matches!(&stops[1].stop, Stop::Engaged(held) if held.reason == "steward freeze"));
// The note names the proven owner, and never the record's claimed `by`.
assert_eq!(
stops[1].note().unwrap(),
format!("steward {} deployment brake engaged: steward freeze", login_name(me)?)
);
// Both engaged, then the steward clears its own: the operator's holds.
replace_stop(operator, "runtime", StopState::Engaged, "operator hold", 11)?;
replace_stop(steward, "runtime", StopState::Clear, "steward thaw", 12)?;
let stops = read_stops(&binding, "runtime");
assert!(matches!(&stops[0].stop, Stop::Engaged(_)) && matches!(&stops[1].stop, Stop::Clear(_)));
assert!(!admits_all(&stops));
// The other order: operator clear, steward engaged still halts.
replace_stop(operator, "runtime", StopState::Clear, "operator thaw", 13)?;
replace_stop(steward, "runtime", StopState::Engaged, "steward again", 14)?;
assert!(!admits_all(&read_stops(&binding, "runtime")));
// Both clear: admits.
replace_stop(steward, "runtime", StopState::Clear, "steward thaw", 15)?;
assert!(admits_all(&read_stops(&binding, "runtime")));
Ok(())
}
#[test]
fn steward_store_owned_by_another_uid_stops() -> Result<()> {
let me = process_uid();
let dir = tempfile::tempdir()?;
// Steward is "nobody" (a real non-root user that is not the writer);
// the file is written by this process, so even a Clear record fails.
if me == 65534 || user_missing("nobody") {
eprintln!("SKIPPED: needs a user `nobody` other than the caller");
return Ok(());
}
let binding = binding_with_steward(dir.path(), "nobody");
let steward = &binding.steward.as_ref().unwrap().store;
replace_stop(steward, "runtime", StopState::Clear, "forged clear", 10)?;
let stops = read_stops(&binding, "runtime");
assert!(matches!(&stops[1].stop, Stop::Unreadable(why) if why.contains("is owned by uid")
&& why.contains("only nobody (the store's owner uid) can repair it")
&& why.contains("stays unreadable")
&& why.contains("idunn-provision deployment-brake-clear")));
// The operator's stop keeps the operator's remedy.
replace_stop(&binding.deployment_store, "other-runtime", StopState::Clear, "x", 11)?;
assert!(matches!(&read_stops(&binding, "runtime")[0].stop, Stop::Unreadable(why)
if why.contains("to replace it run:") && !why.contains("only ")));
assert!(!admits_all(&stops));
Ok(())
}
fn user_missing(name: &str) -> bool {
crate::steward_orders::user_uid(name).is_err()
}
#[test]
fn an_unresolvable_or_root_steward_user_is_an_unreadable_stop_not_an_absent_one() -> Result<()> {
let dir = tempfile::tempdir()?;
for user in ["no-such-login-zz", "root"] {
let binding = binding_with_steward(dir.path(), user);
let stops = read_stops(&binding, "runtime");
assert!(matches!(&stops[1].stop, Stop::Unreadable(_)), "{user}");
assert!(!admits_all(&stops));
}
Ok(())
}
#[test]
fn a_binding_without_a_steward_reads_only_the_operator_store() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut binding = binding_with_steward(dir.path(), "nobody");
binding.steward = None;
let stops = read_stops(&binding, "runtime");
assert_eq!(stops.len(), 1);
assert!(admits_all(&stops));
Ok(())
}
fn store_entry(path: &Path) -> Result<CultCacheEnvelope> {
let mut entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
entries.pop().context("no entry")

View file

@ -28,7 +28,7 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::brake::{Stop, process_uid, read_stop};
use crate::brake::{NamedStop, read_stops};
#[cfg(test)]
use crate::brake::StopState;
use crate::deployment::{
@ -9345,27 +9345,26 @@ impl Engine {
/// Read live at each point that is still reversible: Sealing, the static
/// flip intent and the entry into Fencing.
fn stop_note(&self, current: &Stored<DeploymentTransaction>) -> Result<Option<String>> {
Ok(match self.deployment_stop(current)? {
Stop::Absent | Stop::Clear(_) => None,
Stop::Engaged(held) => Some(format!(
"deployment brake engaged by {}: {}",
held.by, held.reason
)),
Stop::Unreadable(why) => Some(format!(
"deployment brake is unreadable and fails closed: {why}"
)),
})
let notes: Vec<String> = self
.deployment_stops(current)?
.iter()
.filter_map(NamedStop::note)
.collect();
Ok((!notes.is_empty()).then(|| notes.join("; ")))
}
/// What the binding's deployment stop says about this transaction's
/// runtime. Read live; never cached, never consumed.
fn deployment_stop(&self, current: &Stored<DeploymentTransaction>) -> Result<Stop> {
/// What the binding's stops say about this transaction's runtime: the
/// operator's store and, when the binding names one, the steward's. Read
/// live; never cached, never consumed.
fn deployment_stops(
&self,
current: &Stored<DeploymentTransaction>,
) -> Result<Vec<NamedStop>> {
let plan = required(&current.value.plan, "transaction plan")?;
let binding = plan.parsed_inputs()?.1;
Ok(read_stop(
&binding.brakes.deployment_store,
Ok(read_stops(
&binding.brakes,
&binding.runtime_identity.runtime_id,
process_uid(),
))
}
@ -9377,7 +9376,8 @@ impl Engine {
/// hold is a reason to leave a serving or retrying body alone.
fn deploy_can_move(&self, deploy: &Stored<DeploymentTransaction>) -> bool {
deploy.value.deployment_admitted()
|| self.deployment_stop(deploy).is_ok_and(|stop| stop.admits())
|| self.deployment_stops(deploy)
.is_ok_and(|stops| stops.iter().all(|named| named.stop.admits()))
}
/// The deployment a continuity gives way to: one on its target that awaits
@ -26298,6 +26298,13 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
/// that seals it, and a stand-in site beside it.
#[cfg(unix)]
fn static_world() -> Result<StaticWorld> {
static_world_with_steward(None)
}
/// The static world, its binding naming `steward` (a login) as a second
/// stop whose store is `steward/brake.cc`.
#[cfg(unix)]
fn static_world_with_steward(steward: Option<&str>) -> Result<StaticWorld> {
use crate::deployment::tests::{STATIC_BINDING, STATIC_RECIPE};
let tools = TempDir::new()?;
let tree = tools.path().join("tree");
@ -26311,13 +26318,24 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
STATIC_RECIPE,
|root| {
site = root.join("site");
STATIC_BINDING.replace("@SITE@", &site.display().to_string())
let text = STATIC_BINDING.replace("@SITE@", &site.display().to_string());
match steward {
None => text,
Some(user) => text.replace(
"[rollout]",
&format!(
"[brakes.steward]\nuser = \"{user}\"\nstore = \"{}/steward/brake.cc\"\n\n[rollout]",
site.display()
),
),
}
},
Arc::new(InstallingWorkload),
)?;
std::fs::create_dir_all(site.join("www"))?;
std::fs::create_dir_all(site.join("config"))?;
std::fs::create_dir_all(site.join("authority"))?;
std::fs::create_dir_all(site.join("steward"))?;
std::fs::write(site.join("config/site.json"), SITE_CONFIG_ONE)?;
*world.engine.runner_stand_in.lock().unwrap() =
Some(Arc::new(SealingRunner { tree: tree.clone() }));
@ -26451,6 +26469,61 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
Ok(())
}
#[cfg(unix)]
#[test]
fn a_steward_stop_holds_a_deploy_beside_the_operators_and_names_who() -> Result<()> {
let me = crate::brake::process_uid();
if me == 0 {
eprintln!("SKIPPED: a steward is never root, and this run is root");
return Ok(());
}
let user = crate::brake::login_name(me)?;
let w = static_world_with_steward(Some(&user))?;
let steward_store = w.site.join("steward/brake.cc");
let op_store = w.site.join("authority/deployment-brake.cc");
let stop = |store: &Path, state, reason: &str| -> Result<()> {
crate::brake::replace_stop(store, "site-yggdrasil", state, reason, now_millis()?)?;
Ok(())
};
stop(&steward_store, StopState::Engaged, "steward hold")?;
let id = w.transaction_of("up-service")?;
w.tick_until(&id, "the sealed release", |t| t.installed_release.is_some())?;
let settle = |w: &StaticWorld| -> Result<DeploymentTransaction> {
for _ in 0..2 {
w.world.engine.resume_backoff.lock().unwrap().clear();
let _ = w.world.engine.run_scheduler_tick();
}
latest_id(&w.world, &id)
};
let held = settle(&w)?;
assert_eq!(held.phase, DeploymentPhase::Sealing);
assert_eq!(
held.last_error.as_deref(),
Some(format!("steward {user} deployment brake engaged: steward hold").as_str())
);
// The operator engages too; the steward clears its own. The operator's holds.
stop(&op_store, StopState::Engaged, "operator hold")?;
stop(&steward_store, StopState::Clear, "steward thaw")?;
let held = settle(&w)?;
assert!(!held.deployment_admitted());
assert_eq!(
held.last_error.as_deref(),
Some(format!("deployment brake engaged by {user}: operator hold").as_str())
);
// Both engaged: both named.
stop(&steward_store, StopState::Engaged, "steward again")?;
let held = settle(&w)?;
assert!(held.last_error.as_deref().is_some_and(|note| note.contains("operator hold")
&& note.contains("steward again")));
// The operator clears, the steward still holds; then both clear.
stop(&op_store, StopState::Clear, "operator thaw")?;
assert!(!settle(&w)?.deployment_admitted());
stop(&steward_store, StopState::Clear, "steward thaw")?;
w.tick_until(&id, "the end", |t| t.is_terminal())?;
assert!(latest_id(&w.world, &id)?.deployment_admitted());
Ok(())
}
#[cfg(unix)]
#[test]
fn one_clear_admits_many_deploys_and_an_engaged_stop_names_who() -> Result<()> {

View file

@ -753,6 +753,20 @@ impl ProcessWriteLeaseBinding {
pub struct BrakeBinding {
pub deployment_store: PathBuf,
pub lifecycle_store: PathBuf,
/// A second deployment stop, written by the target's steward in a
/// directory only the steward can write. Absent means the operator's store
/// is the only stop.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub steward: Option<StewardBrakeBinding>,
}
/// The steward's own stop: the file's owner uid proves who wrote it.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StewardBrakeBinding {
/// Login name; must resolve to a non-root uid when the store is read.
pub user: String,
pub store: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -1430,6 +1444,39 @@ impl OperatorBinding {
self.brakes.deployment_store != self.brakes.lifecycle_store,
"deployment and lifecycle brakes are identical"
);
if let Some(steward) = &self.brakes.steward {
require_absolute_path(&steward.store, "steward deployment brake")?;
ensure!(
crate::steward_orders::login_grammar_ok(&steward.user) && steward.user != "root",
"steward brake user is invalid or root"
);
ensure!(
steward.store != self.brakes.deployment_store
&& steward.store != self.brakes.lifecycle_store,
"steward brake store is identical to another brake store"
);
// The steward owns its store's directory, so it could unlink any
// file there. Every other authority store must therefore live
// outside that directory, and the steward's directory must be
// clear of every directory an operator store lives in.
let steward_dir = steward.store.parent().unwrap_or(&steward.store);
let others = [
Some(&self.brakes.deployment_store),
Some(&self.brakes.lifecycle_store),
Some(&self.runtime_identity.trust_anchor_store),
self.process_write_lease.as_ref().map(|lease| &lease.record_path),
self.repository.release_authority_store.as_ref(),
];
for other in others.into_iter().flatten() {
let other_dir = other.parent().unwrap_or(other);
ensure!(
!paths_overlap(steward_dir, other_dir),
"steward brake directory {} is shared with or nested in the directory of Idunn authority {}: the steward owns its directory and could remove that file",
steward_dir.display(),
other.display()
);
}
}
self.workload.validate_launch_environment()?;
let workload = match &self.workload {
WorkloadBinding::HostActuator(workload) => {
@ -1462,7 +1509,10 @@ impl OperatorBinding {
&self.brakes.deployment_store,
&self.brakes.lifecycle_store,
&self.runtime_identity.trust_anchor_store,
] {
]
.into_iter()
.chain(self.brakes.steward.as_ref().map(|steward| &steward.store))
{
ensure!(
!paths_overlap(authority, &workload.release_root)
&& !paths_overlap(authority, &workload.current_link),
@ -1531,6 +1581,7 @@ impl OperatorBinding {
self.brakes.deployment_store.clone(),
self.brakes.lifecycle_store.clone(),
];
authority_paths.extend(self.brakes.steward.as_ref().map(|steward| steward.store.clone()));
if let Some(write_lease) = &self.process_write_lease {
authority_paths.push(write_lease.record_path.clone());
authority_paths.push(write_lease.lock_path());
@ -3065,6 +3116,65 @@ nodes = ["yggdrasil"]
text
}
const STEWARD: &str = "\n[brakes.steward]\nuser = \"mind-streampixels\"\nstore = \"/var/lib/gamecult/idunn-steward/streampixels/deployment-brake.cc\"\n";
#[test]
fn binding_without_steward_parses_and_admits_on_operator_store_only() {
for text in [BINDING.to_string(), static_binding(&[])] {
assert_eq!(OperatorBinding::parse(&text).unwrap().brakes.steward, None);
}
let steward = OperatorBinding::parse(&BINDING.replace("\n[rollout]", &format!("{STEWARD}\n[rollout]")))
.unwrap()
.brakes
.steward
.unwrap();
assert_eq!(steward.user, "mind-streampixels");
}
#[test]
fn a_steward_brake_must_be_a_distinct_absolute_non_root_login_and_clear_of_the_workload() {
let with = |steward: &str| BINDING.replace("\n[rollout]", &format!("{steward}\n[rollout]"));
let store = "/var/lib/gamecult/idunn-steward/x/deployment-brake.cc";
for bad in [
format!("\n[brakes.steward]\nuser = \"root\"\nstore = \"{store}\"\n"),
format!("\n[brakes.steward]\nuser = \"Bad User\"\nstore = \"{store}\"\n"),
"\n[brakes.steward]\nuser = \"mind\"\nstore = \"relative.cc\"\n".into(),
"\n[brakes.steward]\nuser = \"mind\"\nstore = \"/var/lib/gamecult/idunn-authority/test-service-deployment-brake.cc\"\n".into(),
"\n[brakes.steward]\nuser = \"mind\"\nstore = \"/var/lib/gamecult/idunn-authority/test-service-lifecycle-brake.cc\"\n".into(),
format!("\n[brakes.steward]\nuser = \"mind\"\nstore = \"{store}\"\nextra = 1\n"),
] {
assert!(OperatorBinding::parse(&with(&bad)).is_err(), "{bad}");
}
// A steward directory shared with, or enclosing, an operator store
// lets the steward unlink a root-owned file: refused.
for (deployment, steward_store) in [
("/var/lib/gamecult/steward-x/op.cc", "/var/lib/gamecult/steward-x/s.cc"),
("/var/lib/gamecult/steward-x/sub/op.cc", "/var/lib/gamecult/steward-x/s.cc"),
("/var/lib/gamecult/op.cc", "/var/lib/gamecult/steward-x/s.cc"),
] {
let shared = BINDING
.replace("/var/lib/gamecult/idunn-authority/test-service-deployment-brake.cc", deployment)
.replace("\n[rollout]", &format!("\n[brakes.steward]\nuser = \"mind\"\nstore = \"{steward_store}\"\n\n[rollout]"));
assert!(OperatorBinding::parse(&shared).is_err(), "{deployment} {steward_store}");
}
// Disjoint directories are the one admitted shape.
let disjoint = BINDING.replace(
"\n[rollout]",
"\n[brakes.steward]\nuser = \"mind\"\nstore = \"/var/lib/gamecult/steward-x/s.cc\"\n\n[rollout]",
);
OperatorBinding::parse(&disjoint).unwrap();
// steward_store_overlap_with_workload_path_refused
let overlapping = BINDING
.replace("\n[rollout]", &format!("\n[brakes.steward]\nuser = \"mind\"\nstore = \"{store}\"\n\n[rollout]"))
.replace(
"cpu_quota_percent = 200\n\n[workload.argument_bindings]",
"cpu_quota_percent = 200\nread_write_paths = [\"/var/lib/gamecult/idunn-steward/x\"]\n\n[workload.argument_bindings]",
);
assert!(OperatorBinding::parse(&overlapping).is_err());
let static_overlap = static_binding(&[("[rollout]", "[brakes.steward]\nuser = \"mind\"\nstore = \"/srv/site/releases/x/brake.cc\"\n\n[rollout]")]);
assert!(OperatorBinding::parse(&static_overlap).is_err());
}
#[test]
fn static_recipe_and_binding_admit() {
let recipe = TargetDeclaration::parse(STATIC_RECIPE).unwrap();

View file

@ -176,12 +176,14 @@ fn deployment_brake_write(options: &BTreeMap<String, String>, state: StopState)
/// Print the stop and exit nonzero unless deployments admit, so a script can
/// gate on it as it could on the retired exact-actuation validation.
fn deployment_brake_status(options: &BTreeMap<String, String>) -> Result<()> {
require_only(options, &["store", "runtime-id"])?;
let stop = read_stop(
&path(options, "store")?,
required(options, "runtime-id")?,
process_uid(),
);
allow_only(options, &["store", "runtime-id", "owner-user"])?;
// A steward's store is owned by the steward: the operator names that user
// to read it, and the owner check is the same one Idunn makes.
let owner = match options.get("owner-user") {
Some(user) => crate::steward_orders::user_uid(user)?,
None => process_uid(),
};
let stop = read_stop(&path(options, "store")?, required(options, "runtime-id")?, owner);
match &stop {
Stop::Absent => println!("absent: deployments admit"),
Stop::Unreadable(why) => println!("unreadable: deployments wait ({why})"),
@ -958,7 +960,7 @@ fn normalized(path: &Path) -> Result<PathBuf> {
}
fn usage() -> &'static str {
"Usage: idunn-provision enroll-provider-health-identity --private-store <path>\n idunn-provision provider-health-public-key --private-store <path>\n idunn-provision export-provider-health-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-idunn-identity --private-store <path>\n idunn-provision export-idunn-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-odin-topology-identity --private-store <path>\n idunn-provision export-odin-topology-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision deployment-brake-engage --store <path> --runtime-id <id> --reason <text>\n idunn-provision deployment-brake-clear --store <path> --runtime-id <id> --reason <text>\n idunn-provision deployment-brake-status --store <path> --runtime-id <id>\n idunn-provision lifecycle-brake-engage --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64>\n idunn-provision lifecycle-brake-release --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64> [--released-until-unix-millis <u64>]\n idunn-provision lifecycle-brake-status --store <path> --runtime-id <id> --target <id> --now-unix-millis <u64>\n idunn-provision create-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision add-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision require-daemon-health-release-binding --store <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex>\n idunn-provision rotate-daemon-health-trust-signer --output <path> --binding-id <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64>\n idunn-provision validate-daemon-health-trust-binding --input <path>\n idunn-provision create-provider-projection-trust-anchor --output <path> --trust-anchor-id <id> --runtime-id <id> --idunn-public-anchor <path> --bound-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision validate-provider-projection-trust-anchor --input <path> --idunn-public-anchor <path>"
"Usage: idunn-provision enroll-provider-health-identity --private-store <path>\n idunn-provision provider-health-public-key --private-store <path>\n idunn-provision export-provider-health-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-idunn-identity --private-store <path>\n idunn-provision export-idunn-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-odin-topology-identity --private-store <path>\n idunn-provision export-odin-topology-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision deployment-brake-engage --store <path> --runtime-id <id> --reason <text>\n idunn-provision deployment-brake-clear --store <path> --runtime-id <id> --reason <text>\n idunn-provision deployment-brake-status --store <path> --runtime-id <id> [--owner-user <login>]\n idunn-provision lifecycle-brake-engage --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64>\n idunn-provision lifecycle-brake-release --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64> [--released-until-unix-millis <u64>]\n idunn-provision lifecycle-brake-status --store <path> --runtime-id <id> --target <id> --now-unix-millis <u64>\n idunn-provision create-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision add-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision require-daemon-health-release-binding --store <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex>\n idunn-provision rotate-daemon-health-trust-signer --output <path> --binding-id <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64>\n idunn-provision validate-daemon-health-trust-binding --input <path>\n idunn-provision create-provider-projection-trust-anchor --output <path> --trust-anchor-id <id> --runtime-id <id> --idunn-public-anchor <path> --bound-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision validate-provider-projection-trust-anchor --input <path> --idunn-public-anchor <path>"
}
#[cfg(test)]

View file

@ -1528,12 +1528,7 @@ impl StewardBinding {
!self.principal.starts_with('-') && !self.principal.contains('/'),
"steward principal is invalid"
);
let user_ok = !self.user.is_empty()
&& self.user.len() <= 32
&& self.user.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
})
&& !self.user.starts_with(|first: char| first.is_ascii_digit() || first == '-');
let user_ok = login_grammar_ok(&self.user);
ensure!(user_ok && self.user != "root", "steward user is invalid or root");
ensure!(
self.mind_control.is_absolute(),
@ -1581,7 +1576,18 @@ fn load_binding(path: &Path) -> Result<StewardBinding> {
StewardBinding::parse(&text)
}
fn user_uid(name: &str) -> Result<u32> {
/// A login name Idunn will resolve: the one grammar for every binding that names
/// a user.
pub(crate) fn login_grammar_ok(user: &str) -> bool {
!user.is_empty()
&& user.len() <= 32
&& user.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
})
&& !user.starts_with(|first: char| first.is_ascii_digit() || first == '-')
}
pub(crate) fn user_uid(name: &str) -> Result<u32> {
let name = std::ffi::CString::new(name)?;
// SAFETY: `passwd` is plain data and getpwnam_r fills it, with strings in
// `buffer`, which outlives every read of them below (none are read).