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);
}
}
}
}