Watch
0
0
Fork
You've already forked hyperhive
0

name the agent's entity before narrowing its role, and test hive provisioning against a fake store

This commit is contained in:
atlas 2026-10-27 10:11:02 +01:00
commit 09d89507a9

View file

@ -13,20 +13,22 @@
//! is ever moved. Any other arrangement is a [`Plan::Conflict`]: nothing is
//! written and an operator decides.
//!
//! Everything else here is best effort: a failure is logged at `warn` and the
//! caller goes on. An agent whose entity is not named after it reads nothing,
//! because the shared policy is the only one its role attaches; at mint the
//! read-back fails, and on a start pass its next reads are refused.
//! An agent whose entity is not named after it reads nothing under the shared
//! policy, so what a failure costs depends on the caller. [`ensure_agent`],
//! which runs before a cert-auth role is narrowed to that policy, returns the
//! error and leaves the role as it was. [`ensure_agents`], the pass at every
//! start, writes no role and so narrows nothing: it logs at `warn` and goes on.
//! The grants are the `identity/` stanzas of `swarm-bao.nix`'s
//! `controllerPolicyText`.
use anyhow::{Context, Result, bail};
use swarm_secret_client::{
SecretStore,
client::{DEFAULT_CERT_MOUNT, EntityRef},
policy,
};
use crate::store::AccessStore;
/// What the store holds for one agent's entity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Seen {
@ -92,13 +94,13 @@ pub fn plan(agent: &str, seen: &Seen) -> Plan {
/// Make sure every agent in `agents` has its entity, logging what could not be
/// done. Never fails: see the module header.
pub async fn ensure_agents(store: &SecretStore, agents: &[String]) {
pub async fn ensure_agents(store: &impl AccessStore, agents: &[String]) {
let accessor = match store.auth_mount_accessor(DEFAULT_CERT_MOUNT).await {
Ok(accessor) => accessor,
Err(e) => {
tracing::warn!(
error = %e,
"agent entities are not managed: the cert auth mount's accessor could not be read; an agent whose entity is not named after it cannot read its secrets"
"agent entities are not managed: the cert auth mount's accessor could not be read; an agent whose role attaches the shared policy alone reads nothing until its entity is named after it"
);
return;
}
@ -120,13 +122,30 @@ pub async fn ensure_agents(store: &SecretStore, agents: &[String]) {
);
}
/// [`ensure_agents`] for one agent.
pub async fn ensure_agent(store: &SecretStore, agent: &str) {
ensure_agents(store, &[agent.to_owned()]).await;
/// Make sure `agent` has its entity, or say why not.
///
/// For a caller that must not go on without it, such as the one that narrows a
/// cert-auth role to the shared policy: an agent whose entity is not named after
/// it reads nothing under that policy.
///
/// # Errors
/// When the cert mount's accessor cannot be read, when a store call fails, and
/// on a [`Plan::Conflict`], which [`ensure_agents`] only logs.
pub async fn ensure_agent(store: &impl AccessStore, agent: &str) -> Result<()> {
let accessor = store
.auth_mount_accessor(DEFAULT_CERT_MOUNT)
.await
.context("reading the cert auth mount's accessor")?;
match ensure_one(store, &accessor, agent).await? {
Plan::Conflict { alias, entity_name } => bail!(
"the alias {alias} is on the entity {entity_name}, which is not {agent}; nothing was changed"
),
_ => Ok(()),
}
}
/// Observe, plan and apply for one agent.
async fn ensure_one(store: &SecretStore, accessor: &str, agent: &str) -> Result<Plan> {
async fn ensure_one(store: &impl AccessStore, accessor: &str, agent: &str) -> Result<Plan> {
let alias = policy::agent_object_name(agent)?;
let seen = Seen {
by_alias: store
@ -174,7 +193,7 @@ async fn ensure_one(store: &SecretStore, accessor: &str, agent: &str) -> Result<
agent,
alias,
entity_name,
"the agent's alias is on an entity that is not the agent's; nothing was changed and the agent cannot read its secrets until an operator resolves it"
"the agent's alias is on an entity that is not the agent's; nothing was changed; under the shared policy alone the agent reads nothing until an operator resolves it"
);
}
}