use chrono::Utc;
use crate::dat::scope::{Scope, ScopeSet};
use crate::dat::token::Dat;
use super::constraints::{default_evaluators, ConstraintEvaluator};
use super::context::EvaluationContext;
use super::decision::{DenialReason, PolicyDecision};
pub struct PolicyEvaluator {
evaluators: Vec<Box<dyn ConstraintEvaluator>>,
}
impl PolicyEvaluator {
pub fn new() -> Self {
Self {
evaluators: default_evaluators(),
}
}
pub fn with_evaluators(evaluators: Vec<Box<dyn ConstraintEvaluator>>) -> Self {
Self { evaluators }
}
pub fn evaluate(&self, dat: &Dat, context: &EvaluationContext) -> PolicyDecision {
let now = Utc::now().timestamp();
if now > dat.claims.exp {
return PolicyDecision::Deny(DenialReason::Expired);
}
if now < dat.claims.nbf {
return PolicyDecision::Deny(DenialReason::NotYetValid);
}
let scope_set = match ScopeSet::parse(&dat.claims.scope) {
Ok(ss) => ss,
Err(_) => {
return PolicyDecision::Deny(DenialReason::ScopeNotCovered);
}
};
let requested = match Scope::parse(&context.requested_scope) {
Ok(s) => s,
Err(_) => {
return PolicyDecision::Deny(DenialReason::ScopeNotCovered);
}
};
if !scope_set.permits(&requested) {
return PolicyDecision::Deny(DenialReason::ScopeNotCovered);
}
if let Some(ref constraints) = dat.claims.constraints {
for evaluator in &self.evaluators {
let decision = evaluator.evaluate(constraints, context);
if decision.is_denied() {
return decision;
}
}
}
PolicyDecision::Allow
}
}
impl Default for PolicyEvaluator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::keys::KeyPair;
use crate::dat::constraints::{DatConstraints, RateLimit};
use crate::trust::level::TrustLevel;
use chrono::{Duration, Utc};
fn issue_test_dat(scope: Vec<String>, constraints: Option<DatConstraints>) -> Dat {
let issuer_kp = KeyPair::generate();
let subject_kp = KeyPair::generate();
let issuer_did = format!(
"did:aid:test:{}",
hex::encode(&issuer_kp.public_key_bytes()[..8])
);
let subject_did = format!(
"did:aid:test:{}",
hex::encode(&subject_kp.public_key_bytes()[..8])
);
let expires = Utc::now() + Duration::hours(24);
Dat::issue(
&issuer_did,
&subject_did,
scope,
expires,
constraints,
None,
&issuer_kp,
)
.expect("failed to issue test DAT")
}
#[test]
fn test_policy_evaluator_allow() {
let dat = issue_test_dat(vec!["mcp:tool:filesystem:read".into()], None);
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read").build();
let pe = PolicyEvaluator::new();
assert!(pe.evaluate(&dat, &ctx).is_allowed());
}
#[test]
fn test_policy_evaluator_deny_scope() {
let dat = issue_test_dat(vec!["mcp:tool:filesystem:read".into()], None);
let ctx = EvaluationContext::builder("mcp:tool:filesystem:write").build();
let pe = PolicyEvaluator::new();
let d = pe.evaluate(&dat, &ctx);
assert!(d.is_denied());
assert_eq!(d.denial_reason(), Some(&DenialReason::ScopeNotCovered));
}
#[test]
fn test_policy_evaluator_deny_expired() {
let issuer_kp = KeyPair::generate();
let subject_kp = KeyPair::generate();
let issuer_did = format!(
"did:aid:test:{}",
hex::encode(&issuer_kp.public_key_bytes()[..8])
);
let subject_did = format!(
"did:aid:test:{}",
hex::encode(&subject_kp.public_key_bytes()[..8])
);
let expires = Utc::now() - Duration::hours(1);
let dat = Dat::issue(
&issuer_did,
&subject_did,
vec!["mcp:tool:filesystem:read".into()],
expires,
None,
None,
&issuer_kp,
)
.expect("failed to issue test DAT");
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read").build();
let pe = PolicyEvaluator::new();
let d = pe.evaluate(&dat, &ctx);
assert!(d.is_denied());
assert_eq!(d.denial_reason(), Some(&DenialReason::Expired));
}
#[test]
fn test_policy_evaluator_deny_constraint() {
let constraints = DatConstraints {
rate_limit: Some(RateLimit {
max_actions: 10,
window_secs: 3600,
}),
..Default::default()
};
let dat = issue_test_dat(vec!["mcp:tool:filesystem:read".into()], Some(constraints));
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read")
.actions_this_hour(10)
.build();
let pe = PolicyEvaluator::new();
let d = pe.evaluate(&dat, &ctx);
assert!(d.is_denied());
match d.denial_reason().unwrap() {
DenialReason::RateLimitExceeded { limit_type, .. } => assert_eq!(limit_type, "hourly"),
other => panic!("expected RateLimitExceeded, got {other:?}"),
}
}
#[test]
fn test_policy_evaluator_wildcard_scope() {
let dat = issue_test_dat(vec!["mcp:*:*:*".into()], None);
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read").build();
let pe = PolicyEvaluator::new();
assert!(pe.evaluate(&dat, &ctx).is_allowed());
}
#[test]
fn test_policy_evaluator_short_circuit() {
let constraints = DatConstraints {
rate_limit: Some(RateLimit {
max_actions: 5,
window_secs: 3600,
}),
min_trust_level: Some(3),
..Default::default()
};
let dat = issue_test_dat(vec!["mcp:tool:filesystem:read".into()], Some(constraints));
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read")
.actions_this_hour(10)
.caller_trust_level(TrustLevel::L0)
.build();
let pe = PolicyEvaluator::new();
let d = pe.evaluate(&dat, &ctx);
assert!(d.is_denied());
match d.denial_reason().unwrap() {
DenialReason::RateLimitExceeded { .. } => {} other => panic!("expected RateLimitExceeded (short-circuit), got {other:?}"),
}
}
#[test]
fn test_policy_evaluator_empty_evaluators() {
let dat = issue_test_dat(
vec!["mcp:tool:filesystem:read".into()],
Some(DatConstraints {
rate_limit: Some(RateLimit {
max_actions: 1,
window_secs: 3600,
}), ..Default::default()
}),
);
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read")
.actions_this_hour(999)
.build();
let pe = PolicyEvaluator::with_evaluators(vec![]); assert!(pe.evaluate(&dat, &ctx).is_allowed());
}
#[test]
fn test_policy_evaluator_multiple_constraints_all_pass() {
let constraints = DatConstraints {
rate_limit: Some(RateLimit {
max_actions: 100,
window_secs: 3600,
}),
min_trust_level: Some(1),
allowed_countries: Some(vec!["AU".into()]),
max_delegation_depth: Some(5),
..Default::default()
};
let dat = issue_test_dat(vec!["mcp:tool:filesystem:read".into()], Some(constraints));
let ctx = EvaluationContext::builder("mcp:tool:filesystem:read")
.actions_this_hour(50)
.caller_trust_level(TrustLevel::L2)
.source_country("AU")
.delegation_depth(2)
.build();
let pe = PolicyEvaluator::new();
assert!(pe.evaluate(&dat, &ctx).is_allowed());
}
}