use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defsandbox-spec")]
pub struct SandboxSpec {
pub name: String,
pub platform: SandboxPlatform,
#[serde(rename = "isolationTier")]
pub isolation_tier: IsolationTier,
#[serde(default, rename = "allowedPaths")]
pub allowed_paths: Vec<String>,
#[serde(default, rename = "networkAllowed")]
pub network_allowed: bool,
#[serde(default, rename = "seccompProfile")]
pub seccomp_profile: Option<String>,
#[serde(default, rename = "userNamespacing")]
pub user_namespacing: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SandboxPlatform {
Linux,
Darwin,
NoSandbox,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationTier {
Strict,
Relaxed,
Permissive,
Off,
}
pub const CANONICAL_SANDBOX_LISP: &str = include_str!("../specs/sandbox.lisp");
pub fn load_canonical() -> Result<Vec<SandboxSpec>, SpecError> {
crate::loader::load_all::<SandboxSpec>(CANONICAL_SANDBOX_LISP)
}
pub fn load_named(name: &str) -> Result<SandboxSpec, SpecError> {
load_canonical()?
.into_iter()
.find(|s| s.name == name)
.ok_or_else(|| SpecError::Load(format!("no (defsandbox-spec) with :name {name:?}")))
}
#[must_use]
pub fn path_allowed(spec: &SandboxSpec, path: &str) -> bool {
spec.allowed_paths.iter().any(|allowed| {
path == allowed || path.starts_with(&format!("{allowed}/"))
})
}
pub fn check_drv_compat(
spec: &SandboxSpec,
requires_network: bool,
requires_no_sandbox: bool,
) -> Result<(), SpecError> {
if requires_no_sandbox && spec.isolation_tier != IsolationTier::Off {
return Err(SpecError::Interp {
phase: "sandbox-policy-violation".into(),
message: format!(
"derivation requires no-sandbox but spec `{}` is tier {:?}",
spec.name, spec.isolation_tier,
),
});
}
if requires_network && !spec.network_allowed {
return Err(SpecError::Interp {
phase: "sandbox-policy-violation".into(),
message: format!(
"derivation requires network but spec `{}` blocks it",
spec.name,
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_sandboxes_parse() {
let specs = load_canonical().expect("canonical sandbox specs must compile");
assert!(!specs.is_empty());
}
#[test]
fn strict_linux_blocks_network() {
let s = load_named("cppnix-linux-strict").unwrap();
assert_eq!(s.platform, SandboxPlatform::Linux);
assert_eq!(s.isolation_tier, IsolationTier::Strict);
assert!(!s.network_allowed);
assert!(s.user_namespacing);
}
#[test]
fn fod_sandbox_allows_network() {
let s = load_named("cppnix-linux-fod").unwrap();
assert!(s.network_allowed);
assert_eq!(s.isolation_tier, IsolationTier::Relaxed);
}
#[test]
fn darwin_sandbox_targets_darwin() {
let s = load_named("cppnix-darwin-strict").unwrap();
assert_eq!(s.platform, SandboxPlatform::Darwin);
}
#[test]
fn path_allowed_matches_exact_and_descendants() {
let s = load_named("cppnix-linux-strict").unwrap();
assert!(path_allowed(&s, "/nix/store"));
assert!(path_allowed(&s, "/nix/store/abc-hello"));
assert!(!path_allowed(&s, "/etc/hosts"));
assert!(!path_allowed(&s, "/home/user"));
}
#[test]
fn strict_sandbox_blocks_network_drv() {
let s = load_named("cppnix-linux-strict").unwrap();
let err = check_drv_compat(&s, true, false).unwrap_err();
match err {
SpecError::Interp { phase, .. } => assert_eq!(phase, "sandbox-policy-violation"),
_ => panic!("expected policy-violation"),
}
}
#[test]
fn fod_sandbox_allows_network_drv() {
let s = load_named("cppnix-linux-fod").unwrap();
check_drv_compat(&s, true, false).unwrap();
}
#[test]
fn no_sandbox_drv_blocked_by_strict_spec() {
let s = load_named("cppnix-linux-strict").unwrap();
let err = check_drv_compat(&s, false, true).unwrap_err();
match err {
SpecError::Interp { phase, .. } => assert_eq!(phase, "sandbox-policy-violation"),
_ => panic!("expected policy-violation"),
}
}
}