use tatara_lisp_eval::Interpreter;
use super::ScriptCtx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Capability {
Pure,
Ambient,
FsRead,
FsWrite,
Env,
HostInfo,
Net,
ClusterCredentials,
Secrets,
Subprocess,
ModuleLoad,
}
impl Capability {
#[must_use]
pub fn escapes_process(self) -> bool {
!matches!(self, Self::Pure | Self::Ambient)
}
}
pub struct Family {
pub name: &'static str,
pub capability: Capability,
install: fn(&mut Interpreter<ScriptCtx>),
}
#[must_use]
pub fn families() -> Vec<Family> {
use super::{
cli, crypto_extra, dns, encoding, env, fs, hash, http, http_server, io, json, kube,
list_ext, log, module, os, process, regex, sops, string, string_ext, time, toml, uuid,
yaml,
};
vec![
Family { name: "cli", capability: Capability::Pure, install: cli::install },
Family { name: "encoding", capability: Capability::Pure, install: encoding::install },
Family { name: "hash", capability: Capability::Pure, install: hash::install },
Family { name: "json", capability: Capability::Pure, install: json::install },
Family { name: "list_ext", capability: Capability::Pure, install: list_ext::install },
Family { name: "log", capability: Capability::Pure, install: log::install },
Family { name: "regex", capability: Capability::Pure, install: regex::install },
Family { name: "string", capability: Capability::Pure, install: string::install },
Family { name: "string_ext", capability: Capability::Pure, install: string_ext::install },
Family { name: "toml", capability: Capability::Pure, install: toml::install },
Family { name: "yaml", capability: Capability::Pure, install: yaml::install },
Family { name: "crypto_extra", capability: Capability::Ambient, install: crypto_extra::install },
Family { name: "time", capability: Capability::Ambient, install: time::install },
Family { name: "uuid", capability: Capability::Ambient, install: uuid::install },
Family { name: "fs", capability: Capability::FsWrite, install: fs::install },
Family { name: "io", capability: Capability::FsWrite, install: io::install },
Family { name: "env", capability: Capability::Env, install: env::install },
Family { name: "os", capability: Capability::HostInfo, install: os::install },
Family { name: "http", capability: Capability::Net, install: http::install },
Family { name: "http_server", capability: Capability::Net, install: http_server::install },
Family { name: "dns", capability: Capability::Net, install: dns::install },
Family { name: "kube", capability: Capability::ClusterCredentials, install: kube::install },
Family { name: "sops", capability: Capability::Secrets, install: sops::install },
Family { name: "process", capability: Capability::Subprocess, install: process::install },
Family { name: "module", capability: Capability::ModuleLoad, install: module::install },
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
pub name: &'static str,
allowed: Vec<Capability>,
}
impl Profile {
#[must_use]
pub fn ambient() -> Self {
Self {
name: "ambient",
allowed: vec![
Capability::Pure,
Capability::Ambient,
Capability::FsRead,
Capability::FsWrite,
Capability::Env,
Capability::HostInfo,
Capability::Net,
Capability::ClusterCredentials,
Capability::Secrets,
Capability::Subprocess,
Capability::ModuleLoad,
],
}
}
#[must_use]
pub fn sealed() -> Self {
Self {
name: "sealed",
allowed: vec![Capability::Pure],
}
}
#[must_use]
pub fn sealed_nondeterministic() -> Self {
Self {
name: "sealed-nondeterministic",
allowed: vec![Capability::Pure, Capability::Ambient],
}
}
#[must_use]
pub fn allows(&self, c: Capability) -> bool {
self.allowed.contains(&c)
}
#[must_use]
pub fn granted(&self) -> Vec<&'static str> {
families()
.into_iter()
.filter(|f| self.allows(f.capability))
.map(|f| f.name)
.collect()
}
#[must_use]
pub fn withheld(&self) -> Vec<&'static str> {
families()
.into_iter()
.filter(|f| !self.allows(f.capability))
.map(|f| f.name)
.collect()
}
}
impl Default for Profile {
fn default() -> Self {
Self::ambient()
}
}
pub fn install_families(interp: &mut Interpreter<ScriptCtx>, profile: &Profile) {
for f in families() {
if profile.allows(f.capability) {
(f.install)(interp);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_catalogue_covers_every_ffi_family() {
let names: Vec<&str> = families().iter().map(|f| f.name).collect();
assert_eq!(
names.len(),
25,
"catalogue has {} families: {names:?}",
names.len()
);
for expected in [
"fs", "io", "env", "os", "process", "http", "http_server", "dns", "kube", "sops",
"module",
] {
assert!(names.contains(&expected), "{expected} missing from the catalogue");
}
}
#[test]
fn a_sealed_profile_grants_nothing_that_escapes_the_process() {
let sealed = Profile::sealed();
let all = families();
assert!(!all.is_empty(), "empty catalogue — the gate would be vacuous");
let mut examined = 0;
for f in &all {
examined += 1;
if f.capability.escapes_process() {
assert!(
!sealed.allows(f.capability),
"sealed profile grants {} ({:?}), which reaches outside the process",
f.name,
f.capability
);
}
}
assert_eq!(examined, 25, "examined {examined} families, expected 25");
let withheld = sealed.withheld();
for dangerous in ["process", "fs", "io", "env", "kube", "sops", "http", "dns"] {
assert!(
withheld.contains(&dangerous),
"sealed profile must withhold {dangerous}; withheld = {withheld:?}"
);
}
}
#[test]
fn the_ambient_profile_grants_everything_so_no_existing_embedder_changes() {
let ambient = Profile::ambient();
assert!(
ambient.withheld().is_empty(),
"ambient must withhold nothing, withheld = {:?}",
ambient.withheld()
);
assert_eq!(ambient.granted().len(), families().len());
}
#[test]
fn sealed_nondeterministic_adds_only_the_clock_and_the_random_pool() {
let p = Profile::sealed_nondeterministic();
assert!(p.granted().contains(&"time"));
assert!(p.granted().contains(&"uuid"));
for f in families() {
if f.capability.escapes_process() {
assert!(!p.allows(f.capability), "{} leaked", f.name);
}
}
}
}