swarm: let every hive read every agent's credential, and say so
A hive reads its agents' credentials with its own certificate, and nothing said which paths that certificate may read, so the read half of a delivery answered 403. The grant is wide on purpose. An agent's path does not name the hive hosting it -- agents move -- so a per-hive grant has to be an enumeration the controller re-emits whenever the roster changes, and an enumeration that can drift or land out of order advertises a boundary it does not hold. A wide grant that says what it is beats a narrow one that only looks narrow. mara's call, on the PR: rather a too-lax scope than one that pretends to be strict. What that buys, beyond honesty: the document is identical for every hive and depends on nothing, so it is written once at startup beside the rest of a hive's provisioning instead of on every declaration. No derived state, no re-emission, and the ordering hazard that came with one stops existing. What still holds is read-only. A hive cannot write an agent's credential, so it cannot hand itself an agent's identity, and the grant reaches nothing in the store outside the agent-credential prefix. The fact is documented where someone meets the boundary rather than only in this message, and the two ways to narrow it later -- scope per hive, or give agents their own store identity -- are tracked.
This commit is contained in:
parent
e638db262e
commit
3752482524
5 changed files with 119 additions and 306 deletions
|
|
@ -1,47 +1,48 @@
|
|||
//! Publishing the grant that lets a hive read its own agents' credentials.
|
||||
//! Writing the policy a hive's own certificate logs in with.
|
||||
//!
|
||||
//! The controller writes a credential; the hive fetches it with its own
|
||||
//! certificate. Nothing said which paths that certificate may read, so the
|
||||
//! read half of every delivery answers 403.
|
||||
//!
|
||||
//! The grant is derived state: it is re-rendered from the swarm's declaration
|
||||
//! whenever that declaration changes ([`crate::wanted`]). A missed re-emission
|
||||
//! is that same untraceable 403, which is why the sink is a trait — "did it
|
||||
//! emit, and with what" is then a test rather than care.
|
||||
//! A startup pass, not a hook: the document is the same for every hive and
|
||||
//! does not depend on which agents exist ([`swarm_secret_client::policy`]
|
||||
//! explains why it is that wide), so there is nothing to keep in step with
|
||||
//! anything. Per-hive failures are reported and skipped — one unwritable
|
||||
//! policy should not take down a daemon that serves everything else, and the
|
||||
//! next start retries it.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use swarm_secret_client::policy;
|
||||
use swarm_secret_client::{SecretStore, policy};
|
||||
|
||||
/// Where a rendered read grant goes.
|
||||
#[async_trait]
|
||||
pub trait ReadPolicySink: Send + Sync {
|
||||
/// Grant `hive` read on exactly `agents`, replacing whatever it had.
|
||||
///
|
||||
/// # Errors
|
||||
/// Whatever the implementation cannot do — for the store-backed one, a
|
||||
/// name the controller may not write or a store it cannot reach.
|
||||
async fn publish(&self, hive: &str, agents: &[&str]) -> Result<()>;
|
||||
}
|
||||
|
||||
/// The real sink: renders the policy and writes it into the secret store.
|
||||
/// Give every hive in `hives` the read policy its certificate will carry.
|
||||
///
|
||||
/// Connects per call, like `matrix_account`'s writer does: the controller
|
||||
/// declares an agent's state rarely, and a handle held across a token's
|
||||
/// lifetime is a renewal problem in exchange for nothing.
|
||||
pub struct StoreSink;
|
||||
|
||||
#[async_trait]
|
||||
impl ReadPolicySink for StoreSink {
|
||||
async fn publish(&self, hive: &str, agents: &[&str]) -> Result<()> {
|
||||
let name = policy::hive_object_name(hive)?;
|
||||
let document = policy::render(hive, agents)?;
|
||||
crate::store::connect()
|
||||
.await
|
||||
.context("connecting to the swarm secret store")?
|
||||
.write_policy(&name, &document)
|
||||
.await
|
||||
.with_context(|| format!("writing the read policy {name}"))?;
|
||||
Ok(())
|
||||
/// Does nothing when this deployment has no store identity, which is the
|
||||
/// shape a controller without a secret store runs in.
|
||||
pub async fn ensure_hive_policies(hives: &[String]) {
|
||||
if hives.is_empty() {
|
||||
return;
|
||||
}
|
||||
let store = match crate::store::connect().await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
tracing::info!(reason = %e, "no secret store reachable; hive read policies are not managed here");
|
||||
return;
|
||||
}
|
||||
};
|
||||
for hive in hives {
|
||||
if let Err(e) = write_one(&store, hive).await {
|
||||
tracing::warn!(hive, error = %format!("{e:#}"), "writing this hive's read policy failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One hive's policy: its own name, the shared document.
|
||||
async fn write_one(store: &SecretStore, hive: &str) -> Result<()> {
|
||||
let name = policy::hive_object_name(hive)?;
|
||||
store
|
||||
.write_policy(&name, &policy::render())
|
||||
.await
|
||||
.with_context(|| format!("writing the read policy {name}"))?;
|
||||
tracing::info!(hive, policy = %name, "hive read policy in place");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue