Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-controller/src/agent_entity.rs

299 lines
10 KiB
Rust

//! The identity entity an agent's login lands on, named after the agent.
//!
//! A cert login creates an entity with a generated name and an alias named for
//! the certificate's common name, `hive-agent-<agent>`. The shared policy
//! ([`policy::render_shared_agent`]) reads `{{identity.entity.name}}`, so the
//! entity has to be called `<agent>`. This module makes it so, either by
//! creating the entity and its alias before the first login or by renaming the
//! entity a login already created. The alias is unique per auth mount and
//! name, so there is never a second entity to pick between.
//!
//! An entity's name is its access under the shared policy, so only an entity
//! the store named itself is ever renamed ([`is_generated_name`]) and no alias
//! is ever moved. Any other arrangement is a [`Plan::Conflict`]: nothing is
//! written and an operator decides.
//!
//! Everything else here is best effort. An agent's own policy still grants it
//! its paths, so a store that refuses these calls costs the shared policy and
//! nothing else: the failure is logged at `warn` and the caller goes on. The
//! grants are the `identity/` stanzas of `swarm-bao.nix`'s
//! `controllerPolicyText`, and a controller can start before they deploy.
use anyhow::{Context, Result, bail};
use swarm_secret_client::{
SecretStore,
client::{DEFAULT_CERT_MOUNT, EntityRef},
policy,
};
/// What the store holds for one agent's entity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Seen {
/// The entity that owns the agent's alias on the cert mount.
pub by_alias: Option<EntityRef>,
/// The id of the entity named after the agent.
pub by_name: Option<String>,
}
/// What to do about it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Plan {
/// The alias's entity is already named after the agent.
Keep,
/// A login created the entity under a generated name.
Rename { id: String },
/// An entity named after the agent exists, and the alias is on no entity.
AddAlias { entity_id: String },
/// Neither the entity nor the alias exists.
CreateAndAlias,
/// The alias sits on an entity that is not the agent's: one with a name of
/// someone's choosing, or one while another entity already has the
/// agent's name. Nothing is written.
Conflict { alias: String, entity_name: String },
}
/// Whether `name` is one the store gave an entity at login: `entity_`, eight
/// lowercase hex digits, and the root namespace's `.root` suffix, which stores
/// that predate namespaces leave off.
///
/// Exact on purpose. A name that is merely close, such as `entity_` followed
/// by a word, is somebody's choice of name.
#[must_use]
pub fn is_generated_name(name: &str) -> bool {
let Some(rest) = name.strip_prefix("entity_") else {
return false;
};
let digits = rest.strip_suffix(".root").unwrap_or(rest);
digits.len() == 8
&& digits
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
/// The plan that leaves the alias on an entity named `agent`.
#[must_use]
pub fn plan(agent: &str, seen: &Seen) -> Plan {
match (&seen.by_alias, &seen.by_name) {
(Some(entity), _) if entity.name == agent => Plan::Keep,
(Some(entity), None) if is_generated_name(&entity.name) => Plan::Rename {
id: entity.id.clone(),
},
(Some(entity), _) => Plan::Conflict {
alias: format!("{}{agent}", policy::AGENT_PREFIX),
entity_name: entity.name.clone(),
},
(None, Some(named)) => Plan::AddAlias {
entity_id: named.clone(),
},
(None, None) => Plan::CreateAndAlias,
}
}
/// 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]) {
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; agents keep their own policy"
);
return;
}
};
let mut changed = 0_usize;
for agent in agents {
match ensure_one(store, &accessor, agent).await {
Ok(Plan::Keep | Plan::Conflict { .. }) => {}
Ok(_) => changed += 1,
Err(e) => {
tracing::warn!(agent, error = %format!("{e:#}"), "this agent's identity entity could not be set up");
}
}
}
tracing::info!(
changed,
agents = agents.len(),
"agent identity entities checked"
);
}
/// [`ensure_agents`] for one agent.
pub async fn ensure_agent(store: &SecretStore, agent: &str) {
ensure_agents(store, &[agent.to_owned()]).await;
}
/// Observe, plan and apply for one agent.
async fn ensure_one(store: &SecretStore, accessor: &str, agent: &str) -> Result<Plan> {
let alias = policy::agent_object_name(agent)?;
let seen = Seen {
by_alias: store
.entity_by_alias(&alias, accessor)
.await
.with_context(|| format!("looking up the entity owning alias {alias}"))?,
by_name: store
.entity_id_by_name(agent)
.await
.with_context(|| format!("looking up the entity named {agent}"))?,
};
let plan = plan(agent, &seen);
match &plan {
Plan::Keep => {}
Plan::Rename { id } => {
store
.rename_entity(id, agent)
.await
.with_context(|| format!("renaming entity {id} to {agent}"))?;
tracing::info!(agent, entity = %id, "agent identity entity renamed");
}
Plan::AddAlias { entity_id } => {
store
.create_entity_alias(&alias, accessor, entity_id)
.await
.with_context(|| format!("adding alias {alias} to entity {entity_id}"))?;
tracing::info!(agent, entity = %entity_id, "agent identity alias added");
}
Plan::CreateAndAlias => {
store
.create_entity(agent)
.await
.with_context(|| format!("creating the entity {agent}"))?;
let Some(entity_id) = store.entity_id_by_name(agent).await? else {
bail!("the entity {agent} was created and is not there to read back");
};
store
.create_entity_alias(&alias, accessor, &entity_id)
.await
.with_context(|| format!("adding alias {alias} to entity {entity_id}"))?;
tracing::info!(agent, entity = %entity_id, "agent identity entity created");
}
Plan::Conflict { alias, entity_name } => {
tracing::error!(
agent,
alias,
entity_name,
"the agent's alias is on an entity that is not the agent's; nothing was changed and the agent keeps its own policy"
);
}
}
Ok(plan)
}
#[cfg(test)]
mod tests {
use super::*;
fn entity(id: &str, name: &str) -> EntityRef {
EntityRef {
id: id.to_owned(),
name: name.to_owned(),
}
}
fn conflict(entity_name: &str) -> Plan {
Plan::Conflict {
alias: "hive-agent-atlas".to_owned(),
entity_name: entity_name.to_owned(),
}
}
#[test]
fn an_entity_already_named_after_the_agent_is_kept() {
let seen = Seen {
by_alias: Some(entity("e1", "atlas")),
by_name: Some("e1".to_owned()),
};
assert_eq!(plan("atlas", &seen), Plan::Keep);
}
#[test]
fn nothing_at_all_is_created_with_its_alias() {
let seen = Seen {
by_alias: None,
by_name: None,
};
assert_eq!(plan("atlas", &seen), Plan::CreateAndAlias);
}
#[test]
fn an_entity_a_login_named_is_renamed() {
for generated in ["entity_1a2b3c4d.root", "entity_1a2b3c4d"] {
let seen = Seen {
by_alias: Some(entity("e1", generated)),
by_name: None,
};
assert_eq!(
plan("atlas", &seen),
Plan::Rename {
id: "e1".to_owned()
},
"{generated}"
);
}
}
#[test]
fn an_entity_somebody_named_is_never_renamed() {
let seen = Seen {
by_alias: Some(entity("e1", "bob")),
by_name: None,
};
assert_eq!(plan("atlas", &seen), conflict("bob"));
}
#[test]
fn a_name_only_close_to_the_generated_shape_is_somebodys_choice() {
for name in [
"entity_",
"entity_backup",
"entity_1a2b3c4",
"entity_1a2b3c4d5",
"entity_1A2B3C4D",
"entity_1a2b3c4g",
"entity_1a2b3c4d.root2",
"entity_1a2b3c4d.other",
"entity_1a2b3c4d.root.root",
"xentity_1a2b3c4d",
"entity-1a2b3c4d",
] {
let seen = Seen {
by_alias: Some(entity("e1", name)),
by_name: None,
};
assert_eq!(plan("atlas", &seen), conflict(name), "{name}");
}
}
#[test]
fn an_entity_without_the_alias_gets_it() {
let seen = Seen {
by_alias: None,
by_name: Some("e1".to_owned()),
};
assert_eq!(
plan("atlas", &seen),
Plan::AddAlias {
entity_id: "e1".to_owned()
}
);
}
#[test]
fn an_alias_on_another_entity_while_the_agents_name_is_taken_is_a_conflict() {
let seen = Seen {
by_alias: Some(entity("e1", "entity_1a2b3c4d.root")),
by_name: Some("e2".to_owned()),
};
assert_eq!(plan("atlas", &seen), conflict("entity_1a2b3c4d.root"));
}
#[test]
fn an_alias_on_a_foreign_entity_is_not_moved_to_the_agents() {
let seen = Seen {
by_alias: Some(entity("e1", "bob")),
by_name: Some("e2".to_owned()),
};
assert_eq!(plan("atlas", &seen), conflict("bob"));
}
}