use serde::{Deserialize, Serialize};
use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum CompiledExpr {
True,
False,
And(Vec<CompiledExpr>),
Or(Vec<CompiledExpr>),
Not(Box<CompiledExpr>),
HasCapability(CanonicalCapability),
HasAllCapabilities(Vec<CanonicalCapability>),
HasAnyCapability(Vec<CanonicalCapability>),
IssuerIs(CanonicalDid),
IssuerIn(Vec<CanonicalDid>),
SubjectIs(CanonicalDid),
DelegatedBy(CanonicalDid),
NotRevoked,
NotExpired,
ExpiresAfter(i64),
IssuedWithin(i64),
RoleIs(String),
RoleIn(Vec<String>),
RepoIs(String),
RepoIn(Vec<String>),
RefMatches(ValidatedGlob),
PathAllowed(Vec<ValidatedGlob>),
EnvIs(String),
EnvIn(Vec<String>),
WorkloadIssuerIs(CanonicalDid),
WorkloadClaimEquals {
key: String,
value: String,
},
IsAgent,
IsHuman,
IsWorkload,
MaxChainDepth(u32),
AttrEquals {
key: String,
value: String,
},
AttrIn {
key: String,
values: Vec<String>,
},
MinAssurance(AssuranceLevel),
AssuranceLevelIs(AssuranceLevel),
ApprovalGate {
inner: Box<CompiledExpr>,
approvers: Vec<CanonicalDid>,
ttl_seconds: u64,
scope: ApprovalScope,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalScope {
#[default]
Identity,
Scoped,
Full,
}
#[derive(Debug, Clone)]
pub struct CompiledPolicy {
expr: CompiledExpr,
source_hash: [u8; 32],
}
impl CompiledPolicy {
pub(crate) fn new(expr: CompiledExpr, source_hash: [u8; 32]) -> Self {
Self { expr, source_hash }
}
pub fn expr(&self) -> &CompiledExpr {
&self.expr
}
pub fn source_hash(&self) -> &[u8; 32] {
&self.source_hash
}
pub fn describe(&self) -> String {
describe_expr(&self.expr, 0)
}
}
fn describe_expr(expr: &CompiledExpr, depth: usize) -> String {
let indent = " ".repeat(depth);
match expr {
CompiledExpr::True => format!("{indent}always allow"),
CompiledExpr::False => format!("{indent}always deny"),
CompiledExpr::And(children) => {
let parts: Vec<String> = children
.iter()
.map(|c| describe_expr(c, depth + 1))
.collect();
format!("{indent}ALL of:\n{}", parts.join("\n"))
}
CompiledExpr::Or(children) => {
let parts: Vec<String> = children
.iter()
.map(|c| describe_expr(c, depth + 1))
.collect();
format!("{indent}ANY of:\n{}", parts.join("\n"))
}
CompiledExpr::Not(inner) => format!("{indent}NOT:\n{}", describe_expr(inner, depth + 1)),
CompiledExpr::HasCapability(c) => format!("{indent}require capability: {c}"),
CompiledExpr::HasAllCapabilities(caps) => {
let names: Vec<String> = caps.iter().map(|c| c.to_string()).collect();
format!("{indent}require all capabilities: [{}]", names.join(", "))
}
CompiledExpr::HasAnyCapability(caps) => {
let names: Vec<String> = caps.iter().map(|c| c.to_string()).collect();
format!("{indent}require any capability: [{}]", names.join(", "))
}
CompiledExpr::IssuerIs(d) => format!("{indent}issuer must be: {d}"),
CompiledExpr::IssuerIn(ds) => {
let names: Vec<String> = ds.iter().map(|d| d.to_string()).collect();
format!("{indent}issuer in: [{}]", names.join(", "))
}
CompiledExpr::SubjectIs(d) => format!("{indent}subject must be: {d}"),
CompiledExpr::DelegatedBy(d) => format!("{indent}delegated by: {d}"),
CompiledExpr::NotRevoked => format!("{indent}not revoked"),
CompiledExpr::NotExpired => format!("{indent}not expired"),
CompiledExpr::ExpiresAfter(s) => format!("{indent}expires after {s}s"),
CompiledExpr::IssuedWithin(s) => format!("{indent}issued within {s}s"),
CompiledExpr::RoleIs(r) => format!("{indent}role must be: {r}"),
CompiledExpr::RoleIn(rs) => format!("{indent}role in: [{}]", rs.join(", ")),
CompiledExpr::RepoIs(r) => format!("{indent}repo must be: {r}"),
CompiledExpr::RepoIn(rs) => format!("{indent}repo in: [{}]", rs.join(", ")),
CompiledExpr::RefMatches(g) => format!("{indent}ref matches: {g}"),
CompiledExpr::PathAllowed(gs) => {
let names: Vec<String> = gs.iter().map(|g| g.to_string()).collect();
format!("{indent}paths allowed: [{}]", names.join(", "))
}
CompiledExpr::EnvIs(e) => format!("{indent}env must be: {e}"),
CompiledExpr::EnvIn(es) => format!("{indent}env in: [{}]", es.join(", ")),
CompiledExpr::WorkloadIssuerIs(d) => format!("{indent}workload issuer: {d}"),
CompiledExpr::WorkloadClaimEquals { key, value } => {
format!("{indent}workload claim {key} = {value}")
}
CompiledExpr::IsAgent => format!("{indent}signer is agent"),
CompiledExpr::IsHuman => format!("{indent}signer is human"),
CompiledExpr::IsWorkload => format!("{indent}signer is workload"),
CompiledExpr::MaxChainDepth(d) => format!("{indent}max chain depth: {d}"),
CompiledExpr::AttrEquals { key, value } => format!("{indent}attr {key} = {value}"),
CompiledExpr::AttrIn { key, values } => {
format!("{indent}attr {key} in: [{}]", values.join(", "))
}
CompiledExpr::MinAssurance(level) => {
format!("{indent}min assurance: {}", level.label())
}
CompiledExpr::AssuranceLevelIs(level) => {
format!("{indent}assurance must be: {}", level.label())
}
CompiledExpr::ApprovalGate {
approvers,
ttl_seconds,
..
} => {
let names: Vec<String> = approvers.iter().map(|d| d.to_string()).collect();
format!(
"{indent}requires approval from [{}] (TTL: {ttl_seconds}s)",
names.join(", ")
)
}
}
}
impl PartialEq for CompiledPolicy {
fn eq(&self, other: &Self) -> bool {
self.expr == other.expr && self.source_hash == other.source_hash
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob};
#[test]
fn compiled_expr_true() {
let expr = CompiledExpr::True;
assert!(matches!(expr, CompiledExpr::True));
}
#[test]
fn compiled_expr_and() {
let expr = CompiledExpr::And(vec![CompiledExpr::True, CompiledExpr::False]);
match expr {
CompiledExpr::And(children) => assert_eq!(children.len(), 2),
_ => panic!("expected And"),
}
}
#[test]
fn compiled_expr_has_capability() {
let cap = CanonicalCapability::parse("sign_commit").unwrap();
let expr = CompiledExpr::HasCapability(cap.clone());
match expr {
CompiledExpr::HasCapability(c) => assert_eq!(c, cap),
_ => panic!("expected HasCapability"),
}
}
#[test]
fn compiled_expr_issuer_is() {
let did = CanonicalDid::parse("did:keri:EOrg123").unwrap();
let expr = CompiledExpr::IssuerIs(did.clone());
match expr {
CompiledExpr::IssuerIs(d) => assert_eq!(d, did),
_ => panic!("expected IssuerIs"),
}
}
#[test]
fn compiled_expr_ref_matches() {
let glob = ValidatedGlob::parse("refs/heads/*").unwrap();
let expr = CompiledExpr::RefMatches(glob.clone());
match expr {
CompiledExpr::RefMatches(g) => assert_eq!(g, glob),
_ => panic!("expected RefMatches"),
}
}
#[test]
fn compiled_policy_accessors() {
let expr = CompiledExpr::True;
let hash = [42u8; 32];
let policy = CompiledPolicy::new(expr.clone(), hash);
assert_eq!(*policy.expr(), expr);
assert_eq!(*policy.source_hash(), hash);
}
#[test]
fn compiled_policy_equality() {
let expr1 = CompiledExpr::True;
let expr2 = CompiledExpr::True;
let hash = [42u8; 32];
let policy1 = CompiledPolicy::new(expr1, hash);
let policy2 = CompiledPolicy::new(expr2, hash);
assert_eq!(policy1, policy2);
}
#[test]
fn compiled_policy_inequality_different_hash() {
let expr = CompiledExpr::True;
let hash1 = [42u8; 32];
let hash2 = [43u8; 32];
let policy1 = CompiledPolicy::new(expr.clone(), hash1);
let policy2 = CompiledPolicy::new(expr, hash2);
assert_ne!(policy1, policy2);
}
}