//! The permissions the swarm grants this hive's agents, as last read from the //! `agent-permissions` bucket by `workers::swarm_permissions`. Stored at //! `/var/lib/hyperhive/swarm-agent-permissions.json`, outside the meta repo: //! the swarm is the record, this file is the hive's last read of it. //! //! Format: agent name → the stored //! [`swarm_queue_client::agent_permissions::AgentPermissions`]. An agent with //! no entry, or an entry with `permissions` unset, holds //! [`Permission::DEFAULT`]; `"permissions": []` holds none. //! //! hive-c0re enforces one member, [`Permission::ManageRootAgent`]: read by //! `lifecycle::host_config::set_nspawn_flags` on every container start, which //! is why a change to it takes a restart. The agent reads the tool members //! itself, and holds its entry here until its own first read succeeds: //! [`hand_at`] copies the entry into the agent's runtime dir. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use hive_types::Permission; use swarm_queue_client::agent_permissions::{AgentPermissions, HIVE_COPY_FILE, HiveCopy}; const FILE: &str = "swarm-agent-permissions.json"; pub type Stored = BTreeMap; #[must_use] pub fn path() -> PathBuf { crate::paths::state_root().join(FILE) } /// Read the stored permissions. An absent file is the empty map: every agent /// on its default. A file that exists but can't be read or parsed is an error. pub fn read_at(path: &Path) -> std::io::Result { super::read_map(path) } /// Persist the permissions atomically. pub fn write_at(path: &Path, stored: &Stored) -> std::io::Result<()> { super::write_map(path, stored) } /// The permissions an agent holds under `record`. #[must_use] pub fn effective(record: Option<&AgentPermissions>) -> Vec { record .and_then(|r| r.permissions.clone()) .unwrap_or_else(|| Permission::DEFAULT.to_vec()) } /// Whether `agent` holds `perm` per the file at `path` (normally [`path`]). /// /// # Errors /// /// As [`read_at`]. pub fn holds(path: &Path, agent: &str, perm: Permission) -> std::io::Result { Ok(effective(read_at(path)?.get(agent)).contains(&perm)) } /// Write `agent`'s [`HiveCopy`] into `runtime_dir`: its entry in the file at /// `stored` (normally [`path`]) and `import_pending`. Left alone when the /// copy there already says the same. /// /// # Errors /// /// As [`read_at`], or the write failed. pub fn hand_at( stored: &Path, import_pending: bool, agent: &str, runtime_dir: &Path, ) -> std::io::Result<()> { let copy = HiveCopy { stored: read_at(stored)?.remove(agent).unwrap_or_default(), import_pending, }; let dest = runtime_dir.join(HIVE_COPY_FILE); if dest.exists() && HiveCopy::read_at(&dest) == copy { return Ok(()); } super::write_map(&dest, ©) } #[cfg(test)] mod tests { use super::*; fn record(json: &str) -> AgentPermissions { serde_json::from_str(json).expect("decodes") } #[test] fn unset_is_the_default_and_an_empty_list_is_none() { assert_eq!(effective(None), Permission::DEFAULT.to_vec()); assert_eq!(effective(Some(&record("{}"))), Permission::DEFAULT.to_vec()); assert!(effective(Some(&record(r#"{"permissions":[]}"#))).is_empty()); assert_eq!( effective(Some(&record(r#"{"permissions":["manage_root_agent"]}"#))), vec![Permission::ManageRootAgent] ); } /// No name holds more than the default without an entry: `ruth` with no /// entry holds `DEFAULT`, and only an entry grants her `manage_root_agent`. #[test] fn ruth_with_no_entry_holds_the_default() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join(FILE); for agent in ["ruth", "atlas"] { for perm in Permission::ALL { assert_eq!( holds(&path, agent, *perm).expect("missing is empty"), Permission::DEFAULT.contains(perm), "{agent} {perm:?}" ); } } write_at( &path, &Stored::from([( "ruth".to_owned(), record(r#"{"permissions":["manage_root_agent"]}"#), )]), ) .expect("seed"); assert!(holds(&path, "ruth", Permission::ManageRootAgent).expect("read")); assert!(!holds(&path, "atlas", Permission::ManageRootAgent).expect("read")); } /// With the queue down nothing is read, so the agent starts on the copy /// written here: the stored grant, not the default. #[test] fn the_agent_is_handed_its_stored_grant_and_whether_the_import_is_pending() { let dir = tempfile::tempdir().expect("tempdir"); let stored = dir.path().join(FILE); let runtime = dir.path().join("run"); write_at( &stored, &Stored::from([( "atlas".to_owned(), record(r#"{"permissions":["web_tools"]}"#), )]), ) .expect("seed"); hand_at(&stored, true, "atlas", &runtime).expect("hand"); let copy = HiveCopy::read_at(&runtime.join(HIVE_COPY_FILE)); assert!(copy.import_pending); assert_eq!(copy.before_read(), vec![Permission::WebTools]); hand_at(&stored, false, "iris", &runtime).expect("hand"); let copy = HiveCopy::read_at(&runtime.join(HIVE_COPY_FILE)); assert_eq!(copy, HiveCopy::default(), "no entry is the default"); } #[test] fn a_corrupt_file_is_an_error() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join(FILE); std::fs::write(&path, "{\n \"ruth\": {\"permissions\": [\"manage_root_ag").expect("seed"); holds(&path, "ruth", Permission::ManageRootAgent).expect_err("truncated JSON"); } }