//! The read agreement: which credentials a hive's own token may fetch. //! //! The mirror of [`crate::matrix`]. That module says 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. //! //! ⚠️ Every hive gets the same document, and it 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`. //! //! Rendering stays separate from writing so the text can be asserted with no //! store to talk to. use crate::{ Error, path::{AGENT_PREFIX, MOUNT, checked_segment}, }; /// Namespace for a hive's own policy and cert-auth role. /// /// 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 policy and cert-auth role name for `hive`. One name, both objects: the /// role attaches the policy by spelling it identically. /// /// # 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}")) } /// Render the document every hive's policy holds: read on every agent's /// credentials. /// /// Takes no arguments because it depends on nothing — same text for every /// hive, unchanged by which agents exist. That is what makes it a deploy-time /// object rather than derived state with a re-emission to get wrong. /// /// Read-only: the controller mints these and never reads one back. #[must_use] pub fn render() -> String { format!("path \"{MOUNT}/data/{AGENT_PREFIX}/*\" {{\n capabilities = [\"read\"]\n}}\n") } #[cfg(test)] mod tests { use super::*; #[test] fn the_document_grants_read_over_the_whole_agent_prefix() { assert_eq!( render(), "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"read\"]\n}\n" ); } #[test] fn the_grant_is_read_only() { // The half of the old policy that survives the widening: a hive reads // credentials, and a hive that could write one could hand itself an // agent's identity. let p = render(); assert!(!p.contains("create")); assert!(!p.contains("update")); assert!(!p.contains("delete")); assert!(!p.contains("list")); } #[test] fn no_name_reaches_the_document_at_all() { // Why the injection cases that used to live here are gone rather than // relaxed: nothing interpolates into the text any more, so there is no // stanza for a name to close. `hive_object_name` still validates, // because a name does reach the policy's *identifier*. let p = render(); assert!(!p.contains("pr1ma")); assert_eq!(p.matches("path \"").count(), 1, "one stanza, no per-agent"); assert!( hive_object_name("atlas/*\" { capabilities = [\"root\"] }").is_err(), "the object NAME is still a place a name can do damage" ); } #[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 every_hive_gets_a_byte_identical_document() { // The property the deploy-time write depends on: nothing about a hive // or its agents changes the text, so there is nothing to re-emit. assert_eq!(render(), render()); } #[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)); } }