164 lines
5.8 KiB
Rust
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, ©)
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
}
|