134 lines
4.8 KiB
Rust
134 lines
4.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_wire::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.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use hive_types::Permission;
|
|
use swarm_wire::agent_permissions::AgentPermissions;
|
|
|
|
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 the agent's traffic may leave the host unfiltered under `record`:
|
|
/// its flag when set, **on** when the agent has no record or the record never
|
|
/// set it.
|
|
#[must_use]
|
|
pub fn unrestricted_internet(record: Option<&AgentPermissions>) -> bool {
|
|
record.is_none_or(swarm_queue_client::agent_permissions::unrestricted)
|
|
}
|
|
|
|
/// 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))
|
|
}
|
|
|
|
#[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]
|
|
);
|
|
}
|
|
|
|
/// An agent with no record, or a record that never set the flag, reads on.
|
|
#[test]
|
|
fn unrestricted_internet_reads_absent_as_on() {
|
|
assert!(unrestricted_internet(None), "no record");
|
|
assert!(unrestricted_internet(Some(&record("{}"))), "flag unset");
|
|
assert!(unrestricted_internet(Some(&record(
|
|
r#"{"unrestricted_internet":true}"#
|
|
))));
|
|
assert!(!unrestricted_internet(Some(&record(
|
|
r#"{"unrestricted_internet":false}"#
|
|
))));
|
|
}
|
|
|
|
/// 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"));
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
}
|