//! 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 { 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-`, so a collision needs a hive named `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/`, /// 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 { 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 { 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//` 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 { 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//`, 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. The stanza cannot constrain the content, and the agent's own /// process holds the identity, so while the leaf is empty it can also create it /// with any text, and the store's value then wins over the file. That is no /// wider than the harness-dir file the agent already owns: /// the container activation's `heal_own_dir` (`nix/agent-modules/user.nix`) /// re-owns that dir to the agent recursively at every boot, so the agent /// already controls the legacy file. 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 { 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 { 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 = [ "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 = ["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"]); } }