Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-secret-client/src/policy.rs
atlas 75e9e205e3 fetch the agent's backend env file from the store, seeded from the existing file
the agent's own root unit reads swarm/agents/<agent>/backend-env into /run and
creates it from backendEnvironmentFile with -cas=0 when the store holds nothing.
the shared agent policy gains create (not update) on that one leaf. the harness
and the subagent daemon load the old file first and the fetched one after it.
2026-10-28 01:24:29 +01:00

690 lines
29 KiB
Rust

//! The read agreement: which credentials a principal's own token may fetch.
//!
//! The mirror of [`crate::matrix`] and [`crate::queue`]. Those modules say
//! where a credential lives; this one says who is allowed to read it, and the
//! two have to agree on the same path or a delivery fails with a 403 that names
//! nothing.
//!
//! The hive document ([`render_shared_hives`]) has one stanza per kind a hive
//! reads, and they are not all scoped alike — which is the point rather than
//! an inconsistency:
//!
//! ⚠️ The **agent** stanza grants read on *every* agent's credentials rather
//! than on the ones that hive hosts. That is a decision, not an oversight: an
//! agent's path does not name its hive, so a per-hive grant has to be
//! enumerated and re-emitted, and an enumeration that can silently drift
//! advertises a boundary it does not hold. A wide grant that says so beats a
//! narrow one that only looks narrow. The narrower shapes, and what they would
//! cost, are in `docs/trust-boundary/security.md`.
//!
//! The **hive** stanza has no such problem and is therefore narrow: that path
//! names its principal, so scoping it to the reader's own name costs nothing
//! and drifts nowhere. Do not widen it to match the agent stanza — the
//! asymmetry is the point, and only holds if a one-per-hive credential is
//! stored under [`Kind::Hive`] — see docs/trust-boundary/security.md.
//!
//! A hive reads nothing under `swarm/services/` or `swarm/controller/`. A swarm
//! service's OIDC client secret is read under a certificate that is not a
//! hive's: `swarm-bao.nix`'s per-service readers, and its granter for the UI's.
//!
//! Rendering stays separate from writing so the text can be asserted with no store to talk to.
use crate::{
Error, backend_env,
path::{Kind, MOUNT, ROOT, checked_segment},
};
/// Namespace for a hive's cert-auth role and the policies the controller writes.
///
/// The controller's own grant is scoped to `hive-*` for both, so this prefix is
/// the difference between a hive the controller may provision and a policy it
/// must not be able to rewrite — including its own.
pub const HIVE_PREFIX: &str = "hive-";
/// The cert-auth role name for `hive`.
///
/// # Errors
/// [`Error::PathSegment`] when `hive` holds anything but `[A-Za-z0-9_-]`.
pub fn hive_object_name(hive: &str) -> Result<String, Error> {
checked_segment("hive", hive)?;
Ok(format!("{HIVE_PREFIX}{hive}"))
}
/// Namespace for an agent's cert-auth role and certificate common name.
///
/// Deliberately *inside* [`HIVE_PREFIX`] rather than beside it.
/// `sys/policies/acl/hive-*` and `auth/cert/certs/hive-*` are the whole of what
/// the controller's grant lets it create (`swarm-bao.nix`'s
/// `controllerPolicyText`), and the controller is the only principal that
/// learns an agent exists — a name outside that prefix is one nothing can
/// write. It costs no authority: the controller already holds `create`/`update`
/// on `secret/data/swarm/agents/*`, so it can already replace every credential
/// the shared policies grant read on.
///
/// It still cannot collide with a hive's. [`hive_object_name`] renders
/// `hive-<hive>`, so a collision needs a hive named `agent-<agent>` — and
/// `nix/reserved-hive-fragments.nix` forbids the substring `agent` in any hive
/// name (and `hive` too), applied to every entry of the swarm directory in
/// `swarm.nix`. The fragment list is what to read before renaming either.
///
/// Host principals' **common names** are operator-set options
/// (`deploy.bao.controllerCommonName`, `secretPublisherCommonName`, …) that
/// may spell this prefix, and that is harmless: an agent's cert-auth role pins
/// the store's agent CA (`deploy.bao.agentPkiMountPath`), which signs no host
/// leaf, and every host role pins `deploy.bao.clientCaFile`, which signs no
/// agent leaf. A CN match alone logs nobody in.
pub const AGENT_PREFIX: &str = "hive-agent-";
/// The cert-auth role name for `agent`, and the common name of the certificate
/// that authenticates it. One string, two objects: the role matches the subject
/// by spelling both the same.
///
/// Injective in `agent` — the prefix is fixed and [`checked_segment`] has
/// already refused anything that could re-punctuate the suffix. Agent names
/// are one swarm-wide namespace (an agent's path is `swarm/agents/<agent>`,
/// with no hive segment), so two agents sharing a name would share every
/// object this names. swarm-controller's `create_agent`, the one place an
/// agent is created, refuses a name the swarm has already placed on another
/// hive. An agent no hive's wanted state declares is invisible to that check.
///
/// # Errors
/// [`Error::PathSegment`] when `agent` holds anything but `[A-Za-z0-9_-]`.
pub fn agent_object_name(agent: &str) -> Result<String, Error> {
checked_segment("agent", agent)?;
Ok(format!("{AGENT_PREFIX}{agent}"))
}
/// The agents among `names`, a listing of cert-auth roles: the inverse of
/// [`agent_object_name`], for a caller that needs the set of agents holding a
/// store identity.
///
/// A name that carries the prefix but whose suffix is not a legal segment is
/// dropped rather than returned: [`agent_object_name`] could not have produced
/// it, so it is not an agent this crate minted.
#[must_use]
pub fn agents_from_role_names(names: &[String]) -> Vec<String> {
names
.iter()
.filter_map(|name| name.strip_prefix(AGENT_PREFIX))
.filter(|agent| checked_segment("agent", agent).is_ok())
.map(str::to_owned)
.collect()
}
/// One read stanza. With [`list_stanza`] and [`create_stanza`], the only shapes
/// this module emits, so what an agent may write is a property of the renderers
/// rather than of each call site.
fn read_stanza(path: &str) -> String {
format!("path \"{path}\" {{\n capabilities = [\"read\"]\n}}\n")
}
/// One list stanza: the key names under a `metadata/` path. Neither a value
/// nor the metadata itself, which take `read` on `data/` and on `metadata/`.
fn list_stanza(path: &str) -> String {
format!("path \"{path}\" {{\n capabilities = [\"list\"]\n}}\n")
}
/// One create stanza: `create` and `read`, never `update`. A leaf that holds
/// nothing takes a first write and the holder cannot replace it afterwards,
/// which is what a `-cas=0` write needs and all it gets.
fn create_stanza(path: &str) -> String {
format!("path \"{path}\" {{\n capabilities = [\"create\", \"read\"]\n}}\n")
}
/// The one policy every hive's cert-auth role attaches.
///
/// Inside [`HIVE_PREFIX`] so that the controller may write it. It is the object
/// name of a hive called `hives`, and `nix/reserved-hive-fragments.nix` forbids
/// the substring `hive` in every hive name, so no hive spells it.
pub const SHARED_HIVE_POLICY: &str = "hive-hives";
/// Render the policy [`SHARED_HIVE_POLICY`] holds: read on every agent's
/// credentials and on this hive's own, with the hive's name taken from the alias
/// of its certificate login on the cert auth mount `mount_accessor`.
///
/// A hive's certificate carries its bare name as the common name, and that is
/// the alias name, so the own-path stanza reads `swarm/hives/<hive>/` for
/// whichever hive presents the token. A login whose alias is not a hive's name
/// reads a path where nothing is stored. The `agents/` and `hives/` segments
/// are fixed text, never taken from the identity.
///
/// The controller kind and the `swarm/services/` tree are deliberately absent:
/// a hive has no business reading the credentials of the thing that provisions
/// it, and a swarm service's OIDC client secret is read under a certificate that
/// is not a hive's. Adding either is a boundary decision, not a consequence of
/// the namespace growing.
///
/// The hive's *own* kind is granted, and only that one: a hive holds the queue
/// credential its own agents authenticate with, so it reads the one principal
/// named after itself, which is why the path takes the name from the alias
/// instead of widening to the whole kind.
///
/// Read-only: the controller mints these and never reads one back.
///
/// # Errors
/// [`Error::PathSegment`] when `mount_accessor` holds anything but
/// `[A-Za-z0-9_-]`: it is interpolated into the policy text, where it could
/// close the stanza.
pub fn render_shared_hives(mount_accessor: &str) -> Result<String, Error> {
checked_segment("mount accessor", mount_accessor)?;
let own = format!("{{{{identity.entity.aliases.{mount_accessor}.name}}}}");
let agents = read_stanza(&format!(
"{MOUNT}/data/{ROOT}/{}/*",
<&str>::from(Kind::Agent)
));
let own = read_stanza(&format!(
"{MOUNT}/data/{ROOT}/{}/{own}/*",
<&str>::from(Kind::Hive)
));
Ok(format!("{agents}{own}"))
}
/// The one policy every agent's cert-auth role attaches.
///
/// Inside [`HIVE_PREFIX`] so that the controller may write it, and without an
/// agent's suffix so that it is never an agent's object name:
/// [`agents_from_role_names`] needs `hive-agent-` with a segment after it. The
/// hive name that would spell it is `agent`, which `nix/reserved-hive-fragments.nix`
/// forbids.
pub const SHARED_AGENT_POLICY: &str = "hive-agent";
/// The template that stands for the agent's name in [`render_shared_agent`]:
/// the name of the login's identity entity, which swarm-controller sets to the
/// agent's name.
const ENTITY_NAME_TEMPLATE: &str = "{{identity.entity.name}}";
/// Render the policy [`SHARED_AGENT_POLICY`] holds: read on one agent's
/// credentials and list on their names (how `hive-matrix-daemon` finds its
/// linked accounts), with the agent's name taken from the identity entity of
/// whoever presents the token.
///
/// The text is the same for every agent, so a change to the rule is one write.
/// An entity whose name is not an agent's grants nothing: the controller names
/// each agent's entity after the agent, and a login that no entity of that
/// name owns reads `swarm/agents/<generated name>/`, where nothing is stored.
///
/// The one write is `create` on the agent's own [`backend_env`] leaf, without
/// `update`: the agent seeds it from the file it has today, once, and cannot
/// replace it. Any other write would let an agent hand itself an identity it
/// was never issued. An agent has no business with another agent's
/// credentials, a hive's, a service's, or the controller's, so none of
/// [`render_shared_hives`]'s breadth applies. A hive still reads this
/// agent's secrets, and closing that is its own decision.
#[must_use]
pub fn render_shared_agent() -> String {
let kind = <&str>::from(Kind::Agent);
let read = read_stanza(&format!(
"{MOUNT}/data/{ROOT}/{kind}/{ENTITY_NAME_TEMPLATE}/*"
));
let list = list_stanza(&format!(
"{MOUNT}/metadata/{ROOT}/{kind}/{ENTITY_NAME_TEMPLATE}/*"
));
let seed = create_stanza(&format!(
"{MOUNT}/data/{ROOT}/{kind}/{ENTITY_NAME_TEMPLATE}/{}",
backend_env::LEAF
));
format!("{read}{list}{seed}")
}
/// What an agent's token grants under [`SHARED_AGENT_POLICY`]: the document
/// with the store's template filled in for `agent`. The oracle tests use to ask
/// whether the policy covers a path.
#[cfg(test)]
pub(crate) fn effective_agent(agent: &str) -> String {
checked_segment("agent", agent).expect("a legal agent name");
render_shared_agent().replace(ENTITY_NAME_TEMPLATE, agent)
}
/// What a hive's token grants under [`SHARED_HIVE_POLICY`]: the document with
/// the store's template filled in for `hive`.
#[cfg(test)]
pub(crate) fn effective_hive(hive: &str) -> String {
checked_segment("hive", hive).expect("a legal hive name");
render_shared_hives("auth_cert_test")
.expect("a legal accessor")
.replace("{{identity.entity.aliases.auth_cert_test.name}}", hive)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_shared_agent_document_reads_the_entity_name_where_the_agent_goes() {
assert_eq!(
render_shared_agent(),
"path \"secret/data/swarm/agents/{{identity.entity.name}}/*\" {\n capabilities = [\"read\"]\n}\n\
path \"secret/metadata/swarm/agents/{{identity.entity.name}}/*\" {\n capabilities = [\"list\"]\n}\n\
path \"secret/data/swarm/agents/{{identity.entity.name}}/backend-env\" {\n capabilities = [\"create\", \"read\"]\n}\n"
);
}
#[test]
fn the_shared_document_names_no_agent_and_grants_no_wider_than_one() {
let shared = render_shared_agent();
assert!(!shared.contains("atlas"));
assert!(!shared.contains("agents/*"), "{shared}");
assert!(!shared.contains("update"));
assert!(!shared.contains("delete"));
}
#[test]
fn the_shared_policy_name_is_writable_and_no_agents_or_hives() {
assert!(SHARED_AGENT_POLICY.starts_with(HIVE_PREFIX));
assert!(
agents_from_role_names(&[SHARED_AGENT_POLICY.to_owned()]).is_empty(),
"it must not be read back as an agent"
);
// The only hive name that would render it carries `agent`.
assert_eq!(
hive_object_name("agent").expect("legal as a segment"),
SHARED_AGENT_POLICY
);
}
#[test]
fn the_document_grants_every_agent_and_this_hive_alone() {
assert_eq!(
effective_hive("pr1ma"),
"path \"secret/data/swarm/agents/*\" {\n capabilities = [\"read\"]\n}\n\
path \"secret/data/swarm/hives/pr1ma/*\" {\n capabilities = [\"read\"]\n}\n"
);
}
#[test]
fn the_shared_hive_document_takes_the_hive_from_the_alias_name() {
let shared = render_shared_hives("auth_cert_1a2b3c4d").expect("legal accessor");
assert_eq!(
shared,
"path \"secret/data/swarm/agents/*\" {\n capabilities = [\"read\"]\n}\n\
path \"secret/data/swarm/hives/{{identity.entity.aliases.auth_cert_1a2b3c4d.name}}/*\" {\n capabilities = [\"read\"]\n}\n"
);
// The template fills in for hives that differ in length, case and
// punctuation, and each then reads exactly its own path.
for hive in ["pr1ma", "a", "Mixed_Case-1", "x".repeat(63).as_str()] {
let own = effective_hive(hive);
assert!(own.contains(&format!("swarm/hives/{hive}/*")), "{own}");
assert!(!own.contains("{{"), "{own}");
}
}
#[test]
fn the_shared_hive_document_names_no_hive_and_grants_no_wider_than_one() {
let shared = render_shared_hives("auth_cert_1a2b3c4d").expect("legal accessor");
assert!(!shared.contains("pr1ma"));
assert!(!shared.contains("hives/*"), "{shared}");
assert!(!shared.contains("create"));
assert!(!shared.contains("update"));
assert!(!shared.contains("delete"));
assert!(!shared.contains("list"));
// The control: the walk finds the stanzas that are there.
assert_eq!(stanza_paths(&shared).len(), 2, "{shared}");
assert!(
stanza_paths(&shared)
.iter()
.any(|g| g.starts_with("swarm/hives/{{"))
);
}
#[test]
fn an_accessor_that_could_close_the_stanza_is_refused() {
for bad in ["", "a.b", "a}}b", "a\"b", "a/b", "a b"] {
assert!(
matches!(render_shared_hives(bad), Err(Error::PathSegment { .. })),
"{bad:?}"
);
}
}
#[test]
fn the_shared_hive_policy_name_is_writable_and_no_agents() {
assert!(SHARED_HIVE_POLICY.starts_with(HIVE_PREFIX));
assert!(
agents_from_role_names(&[SHARED_HIVE_POLICY.to_owned()]).is_empty(),
"it must not be read back as an agent"
);
assert_ne!(SHARED_HIVE_POLICY, SHARED_AGENT_POLICY);
// The only hive name that would render it carries `hive`.
assert_eq!(
hive_object_name("hives").expect("legal as a segment"),
SHARED_HIVE_POLICY
);
}
#[test]
fn a_hive_reaches_no_swarm_services_secret() {
// A swarm service's OIDC client secret is read under a certificate that
// is not a hive's. Checked by walking the stanzas, so a wider grant
// that happened to cover the tree (`swarm/*`, say) fails too.
let p = effective_hive("pr1ma");
assert!(!p.contains("swarm/services"), "{p}");
let service = "swarm/services/swarm-grafana/oidc/client";
assert!(
!stanza_paths(&p).iter().any(|g| covers(g, service)),
"a hive's document reaches {service}:\n{p}"
);
// The control: the walk does find a path the document grants.
assert!(
stanza_paths(&p)
.iter()
.any(|g| covers(g, "swarm/hives/pr1ma/oidc/client"))
);
}
/// 🩸 The per-hive matrix sender-token scoping, asserted on the rendered
/// document rather than on the path function alone: it is the policy text
/// that decides what a hive may fetch, so "the path is per-hive" only
/// means something if the stanza that reaches it is too.
#[test]
fn a_hive_reaches_its_own_matrix_sender_token_and_no_other_hives() {
let alpha = effective_hive("alpha");
let own = crate::matrix::sender_token_path("alpha").expect("legal");
let other = crate::matrix::sender_token_path("beta").expect("legal");
// Reachable: the hive's own stanza is the prefix of its own path.
assert!(
alpha.contains("path \"secret/data/swarm/hives/alpha/*\""),
"{alpha}"
);
assert!(own.starts_with("swarm/hives/alpha/"), "{own}");
// Unreachable: no stanza in alpha's document is a prefix of beta's
// path. Checked by walking the stanzas rather than by asserting the
// absence of the string "beta", so a future stanza that happened to
// cover it — `swarm/hives/*`, say — would fail this too.
assert!(
!stanza_paths(&alpha)
.iter()
.any(|granted| covers(granted, &other)),
"alpha's document reaches {other}:\n{alpha}"
);
}
/// Every `path "…"` a rendered document grants, with the `secret/data/`
/// ACL prefix stripped so it compares against a store path.
fn stanza_paths(document: &str) -> Vec<String> {
document
.lines()
.filter_map(|line| line.strip_prefix("path \""))
.filter_map(|rest| rest.split('"').next())
.filter_map(|p| p.strip_prefix("secret/data/"))
.map(str::to_owned)
.collect()
}
/// Does a granted policy path cover `path`? Only the trailing-`*` form
/// this module renders, which is the only form it has to understand.
fn covers(granted: &str, path: &str) -> bool {
match granted.strip_suffix('*') {
Some(prefix) => path.starts_with(prefix),
None => granted == path,
}
}
#[test]
fn the_grant_is_read_only() {
// A hive reads credentials; a hive that could write one could hand
// itself an agent's identity.
let p = effective_hive("pr1ma");
assert!(!p.contains("create"));
assert!(!p.contains("update"));
assert!(!p.contains("delete"));
assert!(!p.contains("list"));
}
#[test]
fn one_hives_document_does_not_reach_another_hives_path() {
let a = effective_hive("alpha");
assert!(a.contains("swarm/hives/alpha/*"));
assert!(!a.contains("beta"));
assert!(!a.contains("swarm/hives/*"), "the hive stanza stays narrow");
}
#[test]
fn a_name_that_could_close_the_stanza_is_refused() {
// The name reaches a role name and a certificate subject, so a name that
// could re-punctuate either is refused.
assert!(
hive_object_name("atlas/*\" { capabilities = [\"root\"] }").is_err(),
"the object NAME is a place a name can do damage too"
);
}
#[test]
fn the_legal_charset_is_actually_reachable() {
// The control for the case above: if every name were refused, that
// assertion would pass while proving nothing.
assert!(hive_object_name("a-b_C9").is_ok());
}
#[test]
fn the_object_name_sits_inside_the_namespace_the_controller_may_write() {
// `hive-` is what the controller's own policy scopes both
// `sys/policies/acl/` and `auth/cert/certs/` to, so a name outside it
// is one the controller cannot create at all.
let n = hive_object_name("pr1ma").expect("legal");
assert_eq!(n, "hive-pr1ma");
assert!(n.starts_with(HIVE_PREFIX));
}
/// What `effective_agent("atlas")` must be, byte for byte.
const ATLAS_DOCUMENT: &str = "path \"secret/data/swarm/agents/atlas/*\" {\n capabilities = [\"read\"]\n}\n\
path \"secret/metadata/swarm/agents/atlas/*\" {\n capabilities = [\"list\"]\n}\n\
path \"secret/data/swarm/agents/atlas/backend-env\" {\n capabilities = [\"create\", \"read\"]\n}\n";
#[test]
fn an_agents_document_is_its_stanzas_naming_that_agent() {
assert_eq!(effective_agent("atlas"), ATLAS_DOCUMENT);
}
#[test]
fn an_agents_document_does_not_grant_the_whole_agent_prefix() {
// `render`'s agent stanza is `agents/*` on purpose, and inheriting one
// character of that here would give every agent every other agent's
// credentials while the document still read as per-agent.
let p = effective_agent("atlas");
assert!(
!p.contains("swarm/agents/*"),
"the agent stanza must not widen to the kind: {p}"
);
assert!(p.contains("secret/data/swarm/agents/atlas/*"));
assert!(p.contains("secret/metadata/swarm/agents/atlas/*"));
}
#[test]
fn an_agents_document_reaches_nothing_but_that_agent() {
// Stated as an exhaustive check over the kinds rather than as a list of
// paths, so a kind added to `path::Kind` cannot be granted here by a
// renderer nobody re-read.
let p = effective_agent("atlas");
for kind in Kind::ALL {
let segment = <&str>::from(kind);
let expected = kind == Kind::Agent;
assert_eq!(
p.contains(&format!("swarm/{segment}/")),
expected,
"kind {kind:?} in an agent's document: {p}"
);
}
assert!(!p.contains("argus"), "no other principal is named");
assert_eq!(
p.matches("path \"").count(),
3,
"three stanzas, or the document grants something unaccounted for: {p}"
);
}
#[test]
fn an_agents_document_is_exactly_its_own_stanzas() {
// Pinned byte for byte: an added stanza (a hive's queue credential, a
// second agent) is exactly what a presence check misses.
assert_eq!(effective_agent("atlas"), ATLAS_DOCUMENT);
// The control: the pin discriminates between agents.
assert!(!effective_agent("other").contains("agents/atlas/"));
}
/// The paths of the stanzas in `document` that grant `capability`.
fn paths_granting(document: &str, capability: &str) -> Vec<String> {
let mut paths = Vec::new();
let mut current = "";
for line in document.lines() {
if let Some(rest) = line.strip_prefix("path \"") {
current = rest.split_once('"').map_or("", |(path, _)| path);
} else if line.contains(&format!("\"{capability}\"")) {
paths.push(current.to_owned());
}
}
paths
}
#[test]
fn an_agents_only_write_is_creating_its_own_backend_env() {
// An agent that could write its own credentials could hand itself an
// identity it was never issued. The one leaf it may create is the one
// it seeds from its backend environment file, and `update` on it would
// let the agent replace a value the operator set.
let p = effective_agent("atlas");
let leaf = format!(
"secret/data/{}",
crate::backend_env::env_path("atlas").expect("legal")
);
// The control: the walk finds the stanzas that are there.
assert_eq!(
paths_granting(&p, "read"),
["secret/data/swarm/agents/atlas/*", leaf.as_str()]
);
assert_eq!(paths_granting(&p, "create"), [leaf.as_str()]);
for capability in ["update", "delete", "sudo", "patch"] {
assert!(
!p.contains(capability),
"an agent's document must not grant {capability}: {p}"
);
}
// The exact stanza overrides the glob for its path, so it carries
// `read` itself.
assert!(
p.contains("backend-env\" {\n capabilities = [\"create\", \"read\"]"),
"{p}"
);
}
#[test]
fn an_agent_reads_values_and_lists_names_and_nothing_crosswise() {
// `read` on `data/` is the values; `list` on `metadata/` is the key
// names. `read` on `metadata/` (version history, deletion times) is not
// granted, nor is `list` anywhere but `metadata/`.
let p = effective_agent("atlas");
let stanzas: Vec<&str> = p.split_inclusive("}\n").collect();
assert_eq!(stanzas.len(), 3, "{p}");
// The third is the create stanza, pinned by the test above.
for stanza in &stanzas[..2] {
let on_data = stanza.starts_with("path \"secret/data/");
let on_metadata = stanza.starts_with("path \"secret/metadata/");
assert!(on_data != on_metadata, "{stanza}");
assert_eq!(
stanza.contains("capabilities = [\"read\"]"),
on_data,
"{stanza}"
);
assert_eq!(
stanza.contains("capabilities = [\"list\"]"),
on_metadata,
"{stanza}"
);
}
}
#[test]
fn one_agents_document_does_not_reach_another_agents_path() {
let a = effective_agent("atlas");
assert!(a.contains("swarm/agents/atlas/*"));
assert!(!a.contains("argus"));
}
#[test]
fn an_agent_name_that_could_close_the_stanza_is_refused() {
// The name reaches a role name and a certificate subject, so a name that
// could re-punctuate either is refused.
assert!(agent_object_name("atlas/*\" { capabilities = [\"root\"] }").is_err());
assert!(agent_object_name("").is_err());
// The control: if everything were refused the arms above would pass for
// the wrong reason.
assert!(agent_object_name("a-b_C9").is_ok());
}
#[test]
fn an_agents_object_name_sits_where_the_controller_can_write_it() {
// `hive-*` is the whole of what `controllerPolicyText` lets the
// controller create under `sys/policies/acl/` and `auth/cert/certs/`,
// and the controller is the only principal that learns an agent exists.
let n = agent_object_name("atlas").expect("legal");
assert_eq!(n, "hive-agent-atlas");
assert!(n.starts_with(HIVE_PREFIX));
assert!(n.starts_with(AGENT_PREFIX));
}
#[test]
fn an_agents_object_name_cannot_be_spelled_by_a_legal_hive_name() {
// The collision argument, as an assertion rather than as prose: the
// only hive name that renders an agent's object name is one carrying
// the substring `agent`, which `nix/reserved-hive-fragments.nix`
// forbids — so the guard this leans on is named in a test that fails if
// the prefix is ever changed to something that guard does not cover.
let agent = agent_object_name("atlas").expect("legal");
let colliding_hive = agent
.strip_prefix(HIVE_PREFIX)
.expect("an agent's name is inside the hive namespace");
assert_eq!(colliding_hive, "agent-atlas");
assert_eq!(
hive_object_name(colliding_hive).expect("legal as a path segment"),
agent,
"this is the hive name a collision would need"
);
assert!(
colliding_hive.contains("agent"),
"and it is unrepresentable only because a hive name may not contain `agent`"
);
}
#[test]
fn an_agents_document_is_not_the_hives() {
// A later edit that made one shared document delegate to the other
// would hand an agent the hive's `agents/*` stanza.
let agent = effective_agent("atlas");
let hive = effective_hive("atlas");
assert_ne!(agent, hive);
assert!(!agent.contains("swarm/services/"));
assert!(!agent.contains("swarm/hives/"));
// The control: the hive's document does carry that stanza.
assert!(hive.contains("path \"secret/data/swarm/agents/*\""));
}
#[test]
fn only_agent_roles_come_back_and_without_their_prefix() {
let roles: Vec<String> = [
"hive-agent-atlas",
"hive-pr1ma",
"swarm-controller",
"hive-agent-argus",
]
.map(str::to_owned)
.to_vec();
assert_eq!(agents_from_role_names(&roles), ["atlas", "argus"]);
}
#[test]
fn a_role_agent_object_name_could_not_have_produced_is_dropped() {
// An empty suffix and one carrying a dot are both names the prefix
// matches and no agent can have.
let roles: Vec<String> = ["hive-agent-", "hive-agent-a.b", "hive-agent-ok"]
.map(str::to_owned)
.to_vec();
assert_eq!(agents_from_role_names(&roles), ["ok"]);
}
#[test]
fn the_listing_inverts_agent_object_name() {
let name = agent_object_name("atlas").expect("legal");
assert_eq!(agents_from_role_names(&[name]), ["atlas"]);
}
}