stop writing and attaching per-agent and per-hive bao policies
This commit is contained in:
parent
eac7f2a7c1
commit
139007e656
1 changed files with 253 additions and 419 deletions
|
|
@ -5,8 +5,9 @@
|
|||
//! two have to agree on the same path or a delivery fails with a 403 that names
|
||||
//! nothing.
|
||||
//!
|
||||
//! The document has one stanza per kind a hive reads, and they are not all
|
||||
//! scoped alike — which is the point rather than an inconsistency:
|
||||
//! 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
|
||||
|
|
@ -33,15 +34,14 @@ use crate::{
|
|||
path::{Kind, MOUNT, ROOT, checked_segment},
|
||||
};
|
||||
|
||||
/// Namespace for a hive's own policy and cert-auth role.
|
||||
/// 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 policy and cert-auth role name for `hive`. One name, both objects: the
|
||||
/// role attaches the policy by spelling it identically.
|
||||
/// The cert-auth role name for `hive`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `hive` holds anything but `[A-Za-z0-9_-]`.
|
||||
|
|
@ -50,7 +50,7 @@ pub fn hive_object_name(hive: &str) -> Result<String, Error> {
|
|||
Ok(format!("{HIVE_PREFIX}{hive}"))
|
||||
}
|
||||
|
||||
/// Namespace for an agent's own policy and cert-auth role.
|
||||
/// 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
|
||||
|
|
@ -59,7 +59,7 @@ pub fn hive_object_name(hive: &str) -> Result<String, Error> {
|
|||
/// 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
|
||||
/// this policy grants read on.
|
||||
/// 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
|
||||
|
|
@ -75,9 +75,9 @@ pub fn hive_object_name(hive: &str) -> Result<String, Error> {
|
|||
/// agent leaf. A CN match alone logs nobody in.
|
||||
pub const AGENT_PREFIX: &str = "hive-agent-";
|
||||
|
||||
/// The policy and cert-auth role name for `agent`, and the common name of the
|
||||
/// certificate that authenticates it. One string, three objects: the role
|
||||
/// attaches the policy and matches the subject by spelling all three the same.
|
||||
/// 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
|
||||
|
|
@ -124,39 +124,43 @@ fn list_stanza(path: &str) -> String {
|
|||
format!("path \"{path}\" {{\n capabilities = [\"list\"]\n}}\n")
|
||||
}
|
||||
|
||||
/// Render `hive`'s policy document: read on every agent's credentials and on
|
||||
/// this hive's own.
|
||||
/// The one policy every hive's cert-auth role attaches.
|
||||
///
|
||||
/// The name is the only input, and it is deploy-time — so the document is
|
||||
/// still a deploy-time object rather than derived state with a re-emission to
|
||||
/// get wrong. Nothing about which agents exist changes the text.
|
||||
/// 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.
|
||||
///
|
||||
/// ⚠️ 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 that is the decision the agent-only
|
||||
/// version of this grant said had to be made rather than assumed: a hive holds
|
||||
/// the queue credential its own agents authenticate with, so it has to read the
|
||||
/// one principal named after itself — and only that one, which is why the path
|
||||
/// interpolates the name instead of widening to the whole kind.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `hive` holds anything but `[A-Za-z0-9_-]` — the
|
||||
/// name is interpolated into a policy path, so a name that could close the
|
||||
/// stanza could grant itself anything.
|
||||
pub fn render(hive: &str) -> Result<String, Error> {
|
||||
checked_segment("hive", hive)?;
|
||||
Ok(hive_document(hive))
|
||||
}
|
||||
|
||||
/// The text of [`render`] with `own` as the hive segment: a checked hive name,
|
||||
/// or the template [`render_shared_hives`] puts there.
|
||||
fn hive_document(own: &str) -> String {
|
||||
/// [`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)
|
||||
|
|
@ -165,76 +169,10 @@ fn hive_document(own: &str) -> String {
|
|||
"{MOUNT}/data/{ROOT}/{}/{own}/*",
|
||||
<&str>::from(Kind::Hive)
|
||||
));
|
||||
format!("{agents}{own}")
|
||||
Ok(format!("{agents}{own}"))
|
||||
}
|
||||
|
||||
/// The one policy every hive's cert-auth role attaches next to its own.
|
||||
///
|
||||
/// 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: the text of [`render`], 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.
|
||||
///
|
||||
/// # 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)?;
|
||||
Ok(hive_document(&format!(
|
||||
"{{{{identity.entity.aliases.{mount_accessor}.name}}}}"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Render `agent`'s policy document: read on that one agent's credentials, list
|
||||
/// on their names (how `hive-matrix-daemon` finds its linked accounts), and
|
||||
/// nothing else at all.
|
||||
///
|
||||
/// Two stanzas over one subtree, and the single interpolated name is the whole
|
||||
/// document: an agent authenticating with its own certificate gets a token that
|
||||
/// fetches `swarm/agents/<agent>/…`, lists the keys under it, and is refused
|
||||
/// every other path in the store. That is the point — the reason to give an
|
||||
/// agent an identity is that it then depends on its hive for one file (the
|
||||
/// certificate) rather than for every credential it uses.
|
||||
///
|
||||
/// ⚠️ **None of [`render`]'s breadth is inherited.** A hive's document grants
|
||||
/// read on `swarm/agents/*` — *every* agent's credentials, not the ones that
|
||||
/// hive hosts; the module header says why that is a decision rather than an
|
||||
/// oversight. The reason does not transfer: an agent's path names the agent,
|
||||
/// so scoping costs nothing and drifts nowhere (the argument that keeps
|
||||
/// [`render`]'s hive stanza narrow), and an agent has no business with another
|
||||
/// agent's credentials, a hive's, a service's, or the controller's. Narrow
|
||||
/// here does **not** narrow the hive's: a hive still reads this agent's
|
||||
/// secrets, and closing that is its own decision.
|
||||
/// Nothing here writes, for the reason the hive's does not: an agent that could
|
||||
/// write its own credentials could hand itself an identity it was never issued.
|
||||
///
|
||||
/// ⚠️ Not deploy-time: hive-c0re creates agents at runtime (`hive_c0re::meta`).
|
||||
/// swarm-controller rewrites every agent's document at its own start, so an
|
||||
/// edit here reaches existing agents with the next controller restart.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `agent` holds anything but `[A-Za-z0-9_-]`: the
|
||||
/// name is interpolated into the policy text, where it could close the stanza.
|
||||
pub fn render_agent(agent: &str) -> Result<String, Error> {
|
||||
checked_segment("agent", agent)?;
|
||||
let kind = <&str>::from(Kind::Agent);
|
||||
let read = read_stanza(&format!("{MOUNT}/data/{ROOT}/{kind}/{agent}/*"));
|
||||
let list = list_stanza(&format!("{MOUNT}/metadata/{ROOT}/{kind}/{agent}/*"));
|
||||
Ok(format!("{read}{list}"))
|
||||
}
|
||||
|
||||
/// The one policy every agent's cert-auth role attaches next to its 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:
|
||||
|
|
@ -248,14 +186,21 @@ pub const SHARED_AGENT_POLICY: &str = "hive-agent";
|
|||
/// agent's name.
|
||||
const ENTITY_NAME_TEMPLATE: &str = "{{identity.entity.name}}";
|
||||
|
||||
/// Render the policy [`SHARED_AGENT_POLICY`] holds: the stanzas of
|
||||
/// [`render_agent`], with the agent's name taken from the identity entity of
|
||||
/// whoever presents the token instead of written into the text.
|
||||
/// 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.
|
||||
///
|
||||
/// Nothing here writes: an agent that could write its own credentials could
|
||||
/// 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);
|
||||
|
|
@ -268,23 +213,36 @@ pub fn render_shared_agent() -> String {
|
|||
format!("{read}{list}")
|
||||
}
|
||||
|
||||
/// 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_document_is_the_per_agent_one_with_the_entity_name_for_the_agent() {
|
||||
let shared = render_shared_agent();
|
||||
fn the_shared_agent_document_reads_the_entity_name_where_the_agent_goes() {
|
||||
assert_eq!(
|
||||
shared,
|
||||
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"
|
||||
);
|
||||
// Same shape as the per-agent document, so the rule is one rule.
|
||||
assert_eq!(
|
||||
shared.replace("{{identity.entity.name}}", "atlas"),
|
||||
render_agent("atlas").expect("legal")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -314,28 +272,26 @@ mod tests {
|
|||
#[test]
|
||||
fn the_document_grants_every_agent_and_this_hive_alone() {
|
||||
assert_eq!(
|
||||
render("pr1ma").expect("a plain name is legal"),
|
||||
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_is_each_hives_own_with_the_alias_name_for_the_hive() {
|
||||
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"
|
||||
);
|
||||
// Same text as every hive's own document once the template is filled
|
||||
// in, for hives that differ in length, case and punctuation.
|
||||
// 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()] {
|
||||
assert_eq!(
|
||||
shared.replace("{{identity.entity.aliases.auth_cert_1a2b3c4d.name}}", hive),
|
||||
render(hive).expect("legal"),
|
||||
"{hive}"
|
||||
);
|
||||
let own = effective_hive(hive);
|
||||
assert!(own.contains(&format!("swarm/hives/{hive}/*")), "{own}");
|
||||
assert!(!own.contains("{{"), "{own}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +343,7 @@ mod tests {
|
|||
// 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 = render("pr1ma").expect("legal");
|
||||
let p = effective_hive("pr1ma");
|
||||
assert!(!p.contains("swarm/services"), "{p}");
|
||||
let service = "swarm/services/swarm-grafana/oidc/client";
|
||||
assert!(
|
||||
|
|
@ -408,7 +364,7 @@ mod tests {
|
|||
/// 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 = render("alpha").expect("legal");
|
||||
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");
|
||||
|
||||
|
|
@ -456,7 +412,7 @@ mod tests {
|
|||
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 = render("pr1ma").expect("legal");
|
||||
let p = effective_hive("pr1ma");
|
||||
assert!(!p.contains("create"));
|
||||
assert!(!p.contains("update"));
|
||||
assert!(!p.contains("delete"));
|
||||
|
|
@ -465,32 +421,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn one_hives_document_does_not_reach_another_hives_path() {
|
||||
// Replaces `every_hive_gets_a_byte_identical_document`: the hive stanza
|
||||
// is per-reader now, so identical text is no longer the property. This
|
||||
// is what that test was protecting — that a document says only what the
|
||||
// deploy-time name puts in it.
|
||||
let a = render("alpha").expect("legal");
|
||||
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 the_same_name_still_renders_byte_identically() {
|
||||
// The half of the old property that survives: the text is a function of
|
||||
// the deploy-time name alone, so a re-emission cannot drift.
|
||||
assert_eq!(
|
||||
render("pr1ma").expect("legal"),
|
||||
render("pr1ma").expect("legal")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_that_could_close_the_stanza_is_refused() {
|
||||
// The name reaches the document now, which it did not before — so the
|
||||
// injection case is live again in the policy TEXT, not just in the
|
||||
// policy's identifier.
|
||||
assert!(render("alpha/*\" { capabilities = [\"root\"] }").is_err());
|
||||
// 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"
|
||||
|
|
@ -502,7 +442,6 @@ mod tests {
|
|||
// 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());
|
||||
assert!(render("a-b_C9").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -515,16 +454,13 @@ mod tests {
|
|||
assert!(n.starts_with(HIVE_PREFIX));
|
||||
}
|
||||
|
||||
/// What `render_agent("atlas")` must render, byte for byte.
|
||||
/// 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";
|
||||
|
||||
#[test]
|
||||
fn an_agents_document_is_two_stanzas_naming_that_agent() {
|
||||
assert_eq!(
|
||||
render_agent("atlas").expect("a plain name is legal"),
|
||||
ATLAS_DOCUMENT
|
||||
);
|
||||
assert_eq!(effective_agent("atlas"), ATLAS_DOCUMENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -532,7 +468,7 @@ mod tests {
|
|||
// `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 = render_agent("atlas").expect("legal");
|
||||
let p = effective_agent("atlas");
|
||||
assert!(
|
||||
!p.contains("swarm/agents/*"),
|
||||
"the agent stanza must not widen to the kind: {p}"
|
||||
|
|
@ -546,7 +482,7 @@ mod tests {
|
|||
// 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 = render_agent("atlas").expect("legal");
|
||||
let p = effective_agent("atlas");
|
||||
for kind in Kind::ALL {
|
||||
let segment = <&str>::from(kind);
|
||||
let expected = kind == Kind::Agent;
|
||||
|
|
@ -568,13 +504,9 @@ mod tests {
|
|||
fn an_agents_document_is_exactly_its_own_two_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!(render_agent("atlas").expect("legal"), ATLAS_DOCUMENT);
|
||||
assert_eq!(effective_agent("atlas"), ATLAS_DOCUMENT);
|
||||
// The control: the pin discriminates between agents.
|
||||
assert!(
|
||||
!render_agent("other")
|
||||
.expect("legal")
|
||||
.contains("agents/atlas/")
|
||||
);
|
||||
assert!(!effective_agent("other").contains("agents/atlas/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -582,7 +514,7 @@ mod tests {
|
|||
// An agent that could write its own credentials could hand itself an
|
||||
// identity it was never issued — and `create`/`update` on that path is
|
||||
// exactly what the controller holds, so the wall is the capability.
|
||||
let p = render_agent("atlas").expect("legal");
|
||||
let p = effective_agent("atlas");
|
||||
for capability in ["create", "update", "delete", "sudo", "patch"] {
|
||||
assert!(
|
||||
!p.contains(capability),
|
||||
|
|
@ -596,7 +528,7 @@ mod tests {
|
|||
// `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 = render_agent("atlas").expect("legal");
|
||||
let p = effective_agent("atlas");
|
||||
let stanzas: Vec<&str> = p.split_inclusive("}\n").collect();
|
||||
assert_eq!(stanzas.len(), 2, "{p}");
|
||||
for stanza in stanzas {
|
||||
|
|
@ -618,22 +550,19 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn one_agents_document_does_not_reach_another_agents_path() {
|
||||
let a = render_agent("atlas").expect("legal");
|
||||
let a = effective_agent("atlas");
|
||||
assert!(a.contains("swarm/agents/atlas/*"));
|
||||
assert!(!a.contains("argus"));
|
||||
assert_eq!(a, render_agent("atlas").expect("legal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_name_that_could_close_the_stanza_is_refused() {
|
||||
// Same live injection surface as the hive renderer's: the name reaches
|
||||
// the document text, not just an identifier.
|
||||
assert!(render_agent("atlas/*\" { capabilities = [\"root\"] }").is_err());
|
||||
assert!(render_agent("").is_err());
|
||||
// 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!(render_agent("a-b_C9").is_ok());
|
||||
assert!(agent_object_name("a-b_C9").is_ok());
|
||||
}
|
||||
|
||||
|
|
@ -673,18 +602,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn an_agents_document_is_not_the_hives() {
|
||||
// The pin the two renderers need against each other: a later edit that
|
||||
// made `render_agent` delegate to `render`, or vice versa, would hand
|
||||
// an agent the hive's `agents/*` stanza.
|
||||
let agent = render_agent("atlas").expect("legal");
|
||||
let hive = render("atlas").expect("legal");
|
||||
// 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/"));
|
||||
// And the hive's is untouched by this module's growth: still the two
|
||||
// stanzas `the_document_grants_every_agent_and_this_hive_alone` pins
|
||||
// byte for byte, neither narrowed to make the agent's look consistent
|
||||
// with it.
|
||||
// The control: the hive's document does carry that stanza.
|
||||
assert!(hive.contains("path \"secret/data/swarm/agents/*\""));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue