use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "deftrust-model")]
pub struct TrustModel {
pub name: String,
#[serde(rename = "trustedPublicKeys")]
pub trusted_public_keys: Vec<String>,
#[serde(rename = "trustedSubstituters")]
pub trusted_substituters: Vec<String>,
#[serde(rename = "trustedUsers")]
pub trusted_users: Vec<String>,
pub posture: TrustPosture,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TrustPosture {
Permissive,
MultiUser,
Sealed,
}
pub const CANONICAL_TRUST_LISP: &str = include_str!("../specs/trust_model.lisp");
pub fn load_canonical() -> Result<Vec<TrustModel>, SpecError> {
crate::loader::load_all::<TrustModel>(CANONICAL_TRUST_LISP)
}
pub fn load_named(name: &str) -> Result<TrustModel, SpecError> {
load_canonical()?
.into_iter()
.find(|t| t.name == name)
.ok_or_else(|| SpecError::Load(format!("no (deftrust-model) with :name {name:?}")))
}
#[must_use]
pub fn user_can_build(model: &TrustModel, user: &str) -> bool {
for entry in &model.trusted_users {
if entry == "*" || entry == user {
return true;
}
if entry.starts_with('@') {
return true;
}
}
false
}
#[must_use]
pub fn substituter_trusted(model: &TrustModel, url: &str) -> bool {
model.trusted_substituters.iter().any(|s| s == url)
}
#[must_use]
pub fn key_trusted(model: &TrustModel, key_name: &str) -> bool {
model.trusted_public_keys.iter().any(|k| {
k.split(':').next() == Some(key_name)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_trust_models_parse() {
let models = load_canonical().unwrap();
assert!(!models.is_empty());
}
#[test]
fn three_postures_present() {
let models = load_canonical().unwrap();
let postures: std::collections::HashSet<TrustPosture> =
models.iter().map(|m| m.posture).collect();
for required in [TrustPosture::Permissive, TrustPosture::MultiUser, TrustPosture::Sealed] {
assert!(postures.contains(&required), "missing posture {required:?}");
}
}
#[test]
fn sealed_has_no_substituters() {
let m = load_named("sealed-compliance").unwrap();
assert!(
m.trusted_substituters.is_empty(),
"Sealed posture must have no trusted substituters",
);
}
#[test]
fn permissive_lets_anyone_build() {
let m = load_named("single-user-permissive").unwrap();
assert!(user_can_build(&m, "anyone"));
assert!(user_can_build(&m, "drzzln"));
}
#[test]
fn sealed_only_lets_root_build() {
let m = load_named("sealed-compliance").unwrap();
assert!(user_can_build(&m, "root"));
assert!(!user_can_build(&m, "drzzln"));
}
#[test]
fn cache_nixos_org_is_trusted_in_default_model() {
let m = load_named("multi-user-default").unwrap();
assert!(substituter_trusted(&m, "https://cache.nixos.org"));
assert!(!substituter_trusted(&m, "https://untrusted.example.com"));
}
#[test]
fn cache_nixos_key_recognized_by_name_prefix() {
let m = load_named("multi-user-default").unwrap();
assert!(key_trusted(&m, "cache.nixos.org-1"));
assert!(!key_trusted(&m, "random-untrusted-key"));
}
}