Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-c0re/src/agent_config/permissions.rs

164 lines
5.8 KiB
Rust

//! 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<String, AgentPermissions>;
#[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<Stored> {
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<Permission> {
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<bool> {
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, &copy)
}
#[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");
}
}