use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::capability::{Capability, CapabilitySet, KeyFp};
use crate::error::{Error, Result};
use crate::ledger::anchor::SignerId;
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct CapPolicy {
#[serde(default)]
pub chain: ChainPolicy,
#[serde(default, rename = "word")]
pub words: Vec<WordPolicy>,
#[serde(default, rename = "rule")]
rule_dtos: Vec<RuleDto>,
#[serde(skip)]
rules: Vec<Rule>,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct ChainPolicy {
#[serde(default)]
pub trust_roots: Vec<String>,
#[serde(default)]
pub require_monotonic_timestamps: bool,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct WordPolicy {
pub name: String,
pub required_capability: String,
#[serde(default)]
pub accepted_recipients: Vec<String>,
}
#[derive(Debug, Clone)]
struct Rule {
condition: Condition,
action: Action,
}
#[derive(Debug, Clone)]
enum Condition {
WordIs { name: String },
Always,
}
impl Condition {
fn matches(&self, req: &Request<'_>) -> bool {
match self {
Condition::WordIs { name } => req.word == name,
Condition::Always => true,
}
}
}
#[derive(Debug, Clone)]
enum Action {
Allow,
RequireCapability(Capability),
Deny { reason: String },
}
impl Action {
fn decide(&self, req: &Request<'_>) -> Decision {
match self {
Action::Allow => Decision::Allow,
Action::RequireCapability(cap) => {
if req.held.contains(cap) {
Decision::Allow
} else {
Decision::Deny {
rule: "word.required_capability".into(),
context: format!(
"WORD {} requires {}; caller does not hold it",
req.word,
capability_label(cap)
),
}
}
}
Action::Deny { reason } => Decision::Deny {
rule: "rule.deny".into(),
context: reason.clone(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Decision {
Allow,
Deny { rule: String, context: String },
}
impl Decision {
fn into_result(self) -> Result<()> {
match self {
Decision::Allow => Ok(()),
Decision::Deny { rule, context } => Err(Error::PolicyViolation { rule, context }),
}
}
}
#[derive(Debug)]
struct Request<'a> {
word: &'a str,
held: &'a CapabilitySet,
}
fn capability_label(cap: &Capability) -> String {
match cap {
Capability::Viewer => "viewer".into(),
Capability::Reader => "reader".into(),
Capability::Decryptor(_) => "decryptor".into(),
Capability::Signer(_) => "signer".into(),
Capability::Verifier(_) => "verifier".into(),
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
struct RuleDto {
condition: ConditionDto,
action: ActionDto,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "kind", rename_all = "lowercase")]
enum ConditionDto {
Word { word: String },
Always,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "kind", rename_all = "lowercase")]
enum ActionDto {
Allow,
Require { capability: String },
Deny { reason: String },
}
impl RuleDto {
fn to_rule(&self) -> Result<Rule> {
let condition = match &self.condition {
ConditionDto::Word { word } => Condition::WordIs { name: word.clone() },
ConditionDto::Always => Condition::Always,
};
let action = match &self.action {
ActionDto::Allow => Action::Allow,
ActionDto::Require { capability } => {
Action::RequireCapability(parse_capability(capability).ok_or_else(|| {
Error::PolicyViolation {
rule: "rule.action.require".into(),
context: format!("unknown capability '{capability}' in [[rule]]"),
}
})?)
}
ActionDto::Deny { reason } => Action::Deny {
reason: reason.clone(),
},
};
Ok(Rule { condition, action })
}
}
impl CapPolicy {
pub fn from_toml_str(s: &str) -> Result<Self> {
let parsed: CapPolicy =
toml::from_str(s).map_err(|e| Error::Json(format!("policy parse: {e}")))?;
parsed.compile()
}
pub fn load_file(path: &Path) -> Result<Self> {
let s = std::fs::read_to_string(path)?;
Self::from_toml_str(&s)
}
fn compile(mut self) -> Result<Self> {
let mut rules = Vec::with_capacity(self.words.len() + self.rule_dtos.len());
for w in &self.words {
let cap =
parse_capability(&w.required_capability).ok_or_else(|| Error::PolicyViolation {
rule: "word.required_capability".into(),
context: format!(
"unknown capability '{}' for WORD {}",
w.required_capability, w.name
),
})?;
rules.push(Rule {
condition: Condition::WordIs {
name: w.name.clone(),
},
action: Action::RequireCapability(cap),
});
}
for dto in &self.rule_dtos {
rules.push(dto.to_rule()?);
}
self.rules = rules;
Ok(self)
}
fn evaluate(&self, req: &Request<'_>) -> Decision {
for rule in &self.rules {
if rule.condition.matches(req) {
return rule.action.decide(req);
}
}
Decision::Allow
}
pub fn trust_root_allows(&self, signer: &SignerId) -> bool {
if self.chain.trust_roots.is_empty() {
return true;
}
let formatted = signer.to_string();
self.chain
.trust_roots
.iter()
.any(|root| root.eq_ignore_ascii_case(&formatted))
}
pub fn check_word_capability(&self, word: &str, held: &CapabilitySet) -> Result<()> {
self.evaluate(&Request { word, held }).into_result()
}
}
fn parse_capability(s: &str) -> Option<Capability> {
match s.to_ascii_lowercase().as_str() {
"viewer" => Some(Capability::Viewer),
"reader" => Some(Capability::Reader),
"decryptor" => None,
"signer" => Some(Capability::Signer(KeyFp::from_bytes([0u8; 32]))),
"verifier" => Some(Capability::Verifier(KeyFp::from_bytes([0u8; 32]))),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy_with_words(words: Vec<WordPolicy>) -> CapPolicy {
CapPolicy {
chain: ChainPolicy::default(),
words,
rule_dtos: Vec::new(),
rules: Vec::new(),
}
.compile()
.unwrap()
}
fn policy_with_trust_roots(roots: &[&str]) -> CapPolicy {
CapPolicy {
chain: ChainPolicy {
trust_roots: roots.iter().map(|s| s.to_string()).collect(),
require_monotonic_timestamps: false,
},
words: Vec::new(),
rule_dtos: Vec::new(),
rules: Vec::new(),
}
.compile()
.unwrap()
}
fn word(name: &str, cap: &str) -> WordPolicy {
WordPolicy {
name: name.into(),
required_capability: cap.into(),
accepted_recipients: Vec::new(),
}
}
#[test]
fn empty_trust_roots_accept_everything() {
let p = policy_with_trust_roots(&[]);
let signer: SignerId =
"ed25519:9f3a7b0000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
assert!(p.trust_root_allows(&signer));
}
#[test]
fn trust_roots_check_is_case_insensitive() {
let p = policy_with_trust_roots(&[
"Ed25519:9F3A7B0000000000000000000000000000000000000000000000000000000000",
]);
let signer: SignerId =
"ed25519:9f3a7b0000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
assert!(p.trust_root_allows(&signer));
}
#[test]
fn trust_roots_rejects_unlisted_signer() {
let p = policy_with_trust_roots(&[
"ed25519:aaaa000000000000000000000000000000000000000000000000000000000000",
]);
let signer: SignerId =
"ed25519:bbbb000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
assert!(!p.trust_root_allows(&signer));
}
#[test]
fn check_word_capability_passes_when_held() {
let p = policy_with_words(vec![word("Agent_007", "viewer")]);
let held = CapabilitySet::viewing();
assert!(p.check_word_capability("Agent_007", &held).is_ok());
}
#[test]
fn check_word_capability_fails_when_missing() {
let p = policy_with_words(vec![word("Agent_007", "reader")]);
let held = CapabilitySet::empty();
let err = p.check_word_capability("Agent_007", &held).unwrap_err();
assert!(matches!(err, Error::PolicyViolation { .. }));
assert!(err.to_string().contains("requires reader"), "got: {err}");
}
#[test]
fn unknown_word_passes() {
let p = policy_with_words(vec![word("Agent_007", "viewer")]);
let held = CapabilitySet::viewing();
assert!(p.check_word_capability("UNLISTED", &held).is_ok());
}
#[test]
fn unknown_required_capability_is_a_load_error() {
let p = CapPolicy::from_toml_str(
r#"
[[word]]
name = "X"
required_capability = "superuser"
"#,
);
let err = p.unwrap_err();
assert!(matches!(err, Error::PolicyViolation { .. }));
assert!(err.to_string().contains("unknown capability"));
}
#[test]
fn parses_full_policy_document() {
let s = r#"
[chain]
trust_roots = ["ed25519:9f3a7b0000000000000000000000000000000000000000000000000000000000"]
require_monotonic_timestamps = true
[[word]]
name = "Agent_007"
required_capability = "viewer"
accepted_recipients = ["ml-kem:1c8d2e0000000000000000000000000000000000000000000000000000000000"]
"#;
let p = CapPolicy::from_toml_str(s).unwrap();
assert_eq!(
p.chain.trust_roots,
vec!["ed25519:9f3a7b0000000000000000000000000000000000000000000000000000000000"]
);
assert!(p.chain.require_monotonic_timestamps);
assert_eq!(p.words.len(), 1);
assert_eq!(p.words[0].name, "Agent_007");
assert_eq!(p.words[0].accepted_recipients.len(), 1);
assert!(
p.check_word_capability("Agent_007", &CapabilitySet::viewing())
.is_ok()
);
}
#[test]
fn decryptor_as_required_capability_is_a_load_error() {
let err = CapPolicy::from_toml_str(
r#"
[[word]]
name = "Agent_007"
required_capability = "Decryptor"
"#,
)
.unwrap_err();
assert!(matches!(err, Error::PolicyViolation { .. }));
}
#[test]
fn unknown_top_level_field_rejected() {
let s = "chian = {}\n";
assert!(CapPolicy::from_toml_str(s).is_err());
}
fn rule_toml(condition: &str, action: &str) -> String {
format!("[[rule]]\ncondition = {condition}\naction = {action}\n")
}
#[test]
fn rule_require_enforces_capability() {
let p = CapPolicy::from_toml_str(&rule_toml(
r#"{ kind = "word", word = "Agent_007" }"#,
r#"{ kind = "require", capability = "viewer" }"#,
))
.unwrap();
assert!(
p.check_word_capability("Agent_007", &CapabilitySet::viewing())
.is_ok()
);
let err = p
.check_word_capability("Agent_007", &CapabilitySet::empty())
.unwrap_err();
assert!(err.to_string().contains("requires viewer"));
}
#[test]
fn rule_word_condition_is_exact() {
let p = CapPolicy::from_toml_str(&rule_toml(
r#"{ kind = "word", word = "Agent_007" }"#,
r#"{ kind = "deny", reason = "nope" }"#,
))
.unwrap();
assert!(
p.check_word_capability("Agent_008", &CapabilitySet::empty())
.is_ok()
);
assert!(
p.check_word_capability("Agent_007", &CapabilitySet::empty())
.is_err()
);
}
#[test]
fn rule_always_condition_matches_every_word() {
let p = CapPolicy::from_toml_str(&rule_toml(
r#"{ kind = "always" }"#,
r#"{ kind = "deny", reason = "default deny" }"#,
))
.unwrap();
for w in ["a", "b", "anything"] {
assert!(p.check_word_capability(w, &CapabilitySet::empty()).is_err());
}
}
#[test]
fn rule_allow_unblocks_a_word_under_default_deny() {
let toml = r#"
[[rule]]
condition = { kind = "word", word = "PUBLIC" }
action = { kind = "allow" }
[[rule]]
condition = { kind = "always" }
action = { kind = "deny", reason = "no matching rule" }
"#;
let p = CapPolicy::from_toml_str(toml).unwrap();
assert!(
p.check_word_capability("PUBLIC", &CapabilitySet::empty())
.is_ok()
);
let err = p
.check_word_capability("SECRET", &CapabilitySet::empty())
.unwrap_err();
assert!(err.to_string().contains("no matching rule"));
}
#[test]
fn first_matching_rule_wins() {
let toml = r#"
[[rule]]
condition = { kind = "word", word = "W" }
action = { kind = "allow" }
[[rule]]
condition = { kind = "always" }
action = { kind = "deny", reason = "second" }
"#;
let p = CapPolicy::from_toml_str(toml).unwrap();
assert!(
p.check_word_capability("W", &CapabilitySet::empty())
.is_ok()
);
assert!(
p.check_word_capability("X", &CapabilitySet::empty())
.is_err()
);
}
#[test]
fn no_rules_is_open() {
let p = CapPolicy::from_toml_str("").unwrap();
assert!(
p.check_word_capability("ANY", &CapabilitySet::empty())
.is_ok()
);
}
#[test]
fn word_rules_take_precedence_over_rule_tables() {
let toml = r#"
[[word]]
name = "Agent_007"
required_capability = "viewer"
[[rule]]
condition = { kind = "always" }
action = { kind = "deny", reason = "catch-all" }
"#;
let p = CapPolicy::from_toml_str(toml).unwrap();
assert!(
p.check_word_capability("Agent_007", &CapabilitySet::viewing())
.is_ok(),
"word rule should decide before the catch-all"
);
assert!(
p.check_word_capability("OTHER", &CapabilitySet::empty())
.is_err()
);
}
#[test]
fn unknown_capability_in_rule_is_a_load_error() {
let err = CapPolicy::from_toml_str(&rule_toml(
r#"{ kind = "always" }"#,
r#"{ kind = "require", capability = "superuser" }"#,
))
.unwrap_err();
assert!(err.to_string().contains("unknown capability"));
}
#[test]
fn unknown_field_in_rule_table_rejected() {
let err = CapPolicy::from_toml_str(
r#"
[[rule]]
condition = { kind = "always" }
action = { kind = "allow" }
priority = 1
"#,
);
assert!(err.is_err(), "deny_unknown_fields must reject 'priority'");
}
#[test]
fn unknown_condition_kind_rejected() {
let err = CapPolicy::from_toml_str(&rule_toml(
r#"{ kind = "regex", pattern = ".*" }"#,
r#"{ kind = "allow" }"#,
));
assert!(err.is_err());
}
#[test]
fn load_file_reads_and_compiles_from_disk() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("policy.toml");
std::fs::write(
&path,
r#"
[[word]]
name = "Agent_007"
required_capability = "viewer"
"#,
)
.unwrap();
let p = CapPolicy::load_file(&path).unwrap();
assert_eq!(p.words.len(), 1, "file contents must reach the model");
assert!(
p.check_word_capability("Agent_007", &CapabilitySet::viewing())
.is_ok()
);
assert!(
p.check_word_capability("Agent_007", &CapabilitySet::empty())
.is_err()
);
}
#[test]
fn every_accepted_capability_name_loads() {
for name in ["viewer", "reader", "signer", "verifier"] {
let toml = format!("[[word]]\nname = \"W\"\nrequired_capability = \"{name}\"\n");
let p = CapPolicy::from_toml_str(&toml)
.unwrap_or_else(|e| panic!("{name} must be accepted at load: {e}"));
assert!(
p.check_word_capability("W", &CapabilitySet::empty())
.is_err(),
"{name} rule should deny an empty capability set"
);
}
}
#[test]
fn signer_and_verifier_requirements_use_sentinel_fingerprints() {
let p = policy_with_words(vec![word("W", "signer")]);
assert!(
p.check_word_capability("W", &CapabilitySet::empty())
.is_err()
);
}
}