#![allow(dead_code)]
use super::model::*;
use crate::core::api::types::RunRequest;
use anyhow::Result;
use glob::Pattern;
use std::collections::{BTreeMap, HashMap};
pub struct PolicyEngine {
policy: PolicySet,
compiled_patterns: HashMap<String, Pattern>,
}
impl PolicyEngine {
pub fn new(policy: PolicySet) -> Result<Self> {
let mut compiled_patterns = HashMap::new();
for rule in &policy.rules {
let pattern = Pattern::new(&rule.pattern)?;
compiled_patterns.insert(rule.name.clone(), pattern);
}
Ok(Self {
policy,
compiled_patterns,
})
}
pub fn classify_operation(&self, operation_id: &str) -> DataClassification {
for (pattern_str, classification) in &self.policy.classifications {
if pattern_str == operation_id {
return classification.clone();
}
if let Ok(pattern) = Pattern::new(pattern_str) {
if pattern.matches(operation_id) {
return classification.clone();
}
}
}
self.policy.defaults.default_classification.clone()
}
pub fn evaluate(
&self,
request: &RunRequest,
url: &str,
context: &EvaluationContext,
) -> PolicyDecision {
if self.policy.defaults.require_auth && request.auth_profile.is_none() {
let has_explicit_unauth_allow = self.policy.rules.iter().any(|rule| {
if let Some(pattern) = self.compiled_patterns.get(&rule.name) {
if pattern.matches(url) {
if let Some(_conditions) = &rule.conditions {
return false;
}
if let Some(allow) = &rule.allow {
return self.matches_action(allow, request, context);
}
}
}
false
});
if !has_explicit_unauth_allow {
return PolicyDecision::Deny {
rule: "authentication".to_string(),
reason: "Authentication required - no rule allows unauthenticated access"
.to_string(),
audit: Some(AuditConfig {
level: self.policy.defaults.audit_level.clone(),
include_body: false,
include_response: false,
}),
};
}
}
if self.policy.defaults.read_only {
let is_read = context
.method
.as_ref()
.map(|m| matches!(m.to_uppercase().as_str(), "GET" | "HEAD" | "OPTIONS"))
.unwrap_or(false);
if !is_read {
return PolicyDecision::Deny {
rule: "read_only".to_string(),
reason: "Read-only mode is enabled — all write operations are blocked"
.to_string(),
audit: Some(AuditConfig {
level: self.policy.defaults.audit_level.clone(),
include_body: false,
include_response: false,
}),
};
}
}
for rule in &self.policy.rules {
if let Some(pattern) = self.compiled_patterns.get(&rule.name) {
if pattern.matches(url) {
if let Some(conditions) = &rule.conditions {
if !self.check_conditions(conditions, request, context) {
continue;
}
}
if let Some(deny) = &rule.deny {
if self.matches_action(deny, request, context) {
return PolicyDecision::Deny {
rule: rule.name.clone(),
reason: rule.explain.clone().unwrap_or_else(|| {
format!("Operation denied by rule: {}", rule.name)
}),
audit: rule.audit.clone(),
};
}
}
if let Some(allow) = &rule.allow {
if self.matches_action(allow, request, context) {
if self.policy.defaults.require_auth && request.auth_profile.is_none() {
return PolicyDecision::Deny {
rule: rule.name.clone(),
reason: "Authentication required for this operation"
.to_string(),
audit: rule.audit.clone(),
};
}
return PolicyDecision::Allow {
rule: rule.name.clone(),
audit: rule.audit.clone(),
};
}
}
}
}
}
self.apply_defaults(request, context)
}
fn check_conditions(
&self,
conditions: &[PolicyCondition],
request: &RunRequest,
context: &EvaluationContext,
) -> bool {
conditions.iter().all(|condition| {
if let Some(required_profile) = &condition.auth_profile {
if let Some(auth_profile) = &request.auth_profile {
if auth_profile != required_profile {
return false;
}
} else {
return false;
}
}
if let Some(time_window) = &condition.time_window {
if !self.check_time_window(time_window, context) {
return false;
}
}
if let Some(required_ip) = &condition.source_ip {
if let Some(source_ip) = &context.source_ip {
if !self.matches_ip_pattern(source_ip, required_ip) {
return false;
}
} else {
return false;
}
}
if let Some(required_env) = &condition.environment {
if let Some(env) = &request.env {
if env != required_env {
return false;
}
} else {
return false;
}
}
true
})
}
fn matches_action(
&self,
action: &PolicyAction,
request: &RunRequest,
context: &EvaluationContext,
) -> bool {
if let Some(true) = action.all {
return true;
}
if let Some(operations) = &action.operations {
let matches = operations.iter().any(|op_pattern| {
if let Ok(pattern) = Pattern::new(op_pattern) {
pattern.matches(&request.operation_id)
} else {
op_pattern == &request.operation_id
}
});
if !matches {
return false;
}
}
if let Some(methods) = &action.methods {
if let Some(method) = &context.method {
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
return false;
}
}
}
if let Some(required_tags) = &action.tags {
if let Some(operation_tags) = &context.tags {
let has_required_tag = required_tags
.iter()
.any(|required| operation_tags.contains(required));
if !has_required_tag {
return false;
}
} else if !required_tags.is_empty() {
return false;
}
}
true
}
fn apply_defaults(&self, request: &RunRequest, context: &EvaluationContext) -> PolicyDecision {
if self.policy.defaults.require_auth && request.auth_profile.is_none() {
return PolicyDecision::Deny {
rule: "default".to_string(),
reason: "Authentication required by default policy".to_string(),
audit: Some(AuditConfig {
level: self.policy.defaults.audit_level.clone(),
include_body: false,
include_response: false,
}),
};
}
if let Some(method) = &context.method {
if !self
.policy
.defaults
.allow_methods
.iter()
.any(|m| m.eq_ignore_ascii_case(method))
{
return PolicyDecision::Deny {
rule: "default".to_string(),
reason: format!("Method {} not allowed by default policy", method),
audit: Some(AuditConfig {
level: self.policy.defaults.audit_level.clone(),
include_body: false,
include_response: false,
}),
};
}
}
PolicyDecision::Deny {
rule: "default".to_string(),
reason: "No matching allow rule found".to_string(),
audit: Some(AuditConfig {
level: self.policy.defaults.audit_level.clone(),
include_body: false,
include_response: false,
}),
}
}
fn check_time_window(&self, time_window: &str, _context: &EvaluationContext) -> bool {
match time_window {
"business_hours" => {
true
}
"weekdays" => {
true
}
_ => false,
}
}
fn matches_ip_pattern(&self, ip: &str, pattern: &str) -> bool {
ip == pattern
}
pub fn is_read_only(&self) -> bool {
self.policy.defaults.read_only
}
pub fn max_calls_per_session(&self) -> u32 {
self.policy.defaults.max_calls_per_session
}
pub fn max_calls_per_minute(&self) -> u32 {
self.policy.defaults.max_calls_per_minute
}
pub fn warn_on_confidential(&self) -> bool {
self.policy.defaults.warn_on_confidential_to_llm
}
pub fn block_regulated(&self) -> bool {
self.policy.defaults.block_regulated_to_llm
}
pub fn check_credential_scope(
&self,
auth_profile: &str,
method: &str,
tags: Option<&[String]>,
) -> Option<String> {
let scope = self
.policy
.credential_scopes
.iter()
.find(|s| s.profile == auth_profile)?;
if !scope.allowed_methods.is_empty()
&& !scope
.allowed_methods
.iter()
.any(|m| m.eq_ignore_ascii_case(method))
{
return Some(format!(
"Credential scope '{}' does not allow method {}. Allowed: {:?}",
scope.profile, method, scope.allowed_methods
));
}
if !scope.allowed_tags.is_empty() {
let has_matching_tag = tags
.map(|t| t.iter().any(|tag| scope.allowed_tags.contains(tag)))
.unwrap_or(false);
if !has_matching_tag {
return Some(format!(
"Credential scope '{}' restricts access to tags {:?}",
scope.profile, scope.allowed_tags
));
}
}
if scope
.requires_approval
.iter()
.any(|m| m.eq_ignore_ascii_case(method))
{
return Some(format!(
"Credential scope '{}' requires human approval for {} operations",
scope.profile, method
));
}
None
}
pub fn is_operation_allowed(&self, operation_id: &str, method: &str) -> bool {
self.is_operation_allowed_with_tags(operation_id, method, None)
}
pub fn is_operation_allowed_with_tags(
&self,
operation_id: &str,
method: &str,
tags: Option<&[String]>,
) -> bool {
if self.policy.defaults.read_only {
if !matches!(method.to_uppercase().as_str(), "GET" | "HEAD" | "OPTIONS") {
return false;
}
}
let request = RunRequest {
operation_id: operation_id.to_string(),
auth_profile: Some("mcp".to_string()),
env: None,
parameters: None,
body: None,
spec_path: None,
};
let context = EvaluationContext {
method: Some(method.to_string()),
tags: tags.map(|t| t.to_vec()),
source_ip: None,
timestamp: chrono::Utc::now(),
};
for rule in &self.policy.rules {
if rule.pattern != "*" {
continue;
}
if let Some(deny) = &rule.deny {
if self.matches_action(deny, &request, &context) {
return false;
}
}
if let Some(allow) = &rule.allow {
if self.matches_action(allow, &request, &context) {
return true;
}
}
}
self.policy
.defaults
.allow_methods
.iter()
.any(|m| m.eq_ignore_ascii_case(method))
}
}
#[derive(Debug, Clone)]
pub struct EvaluationContext {
pub method: Option<String>,
pub tags: Option<Vec<String>>,
pub source_ip: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl Default for EvaluationContext {
fn default() -> Self {
Self {
method: None,
tags: None,
source_ip: None,
timestamp: chrono::Utc::now(),
}
}
}
#[derive(Debug, Clone)]
pub enum PolicyDecision {
Allow {
rule: String,
audit: Option<AuditConfig>,
},
Deny {
rule: String,
reason: String,
audit: Option<AuditConfig>,
},
}
impl PolicyDecision {
pub fn is_allowed(&self) -> bool {
matches!(self, PolicyDecision::Allow { .. })
}
pub fn rule_name(&self) -> &str {
match self {
PolicyDecision::Allow { rule, .. } => rule,
PolicyDecision::Deny { rule, .. } => rule,
}
}
pub fn audit_config(&self) -> Option<&AuditConfig> {
match self {
PolicyDecision::Allow { audit, .. } => audit.as_ref(),
PolicyDecision::Deny { audit, .. } => audit.as_ref(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_policy() -> PolicySet {
PolicySet {
version: "1.0".to_string(),
metadata: None,
defaults: PolicyDefaults {
allow_methods: vec!["GET".to_string()],
deny_external_refs: true,
require_auth: true,
audit_level: "basic".to_string(),
default_classification: DataClassification::Internal,
read_only: false,
max_calls_per_session: 0,
max_calls_per_minute: 0,
warn_on_confidential_to_llm: false,
block_regulated_to_llm: false,
},
classifications: BTreeMap::new(),
credential_scopes: vec![],
rules: vec![PolicyRule {
name: "readonly".to_string(),
description: Some("Allow read-only operations".to_string()),
pattern: "*".to_string(),
conditions: None,
allow: Some(PolicyAction {
methods: Some(vec!["GET".to_string()]),
operations: Some(vec!["get*".to_string(), "list*".to_string()]),
all: None,
tags: None,
}),
deny: None,
audit: None,
explain: None,
}],
}
}
#[test]
fn test_allow_readonly_operation() {
let policy = create_test_policy();
let engine = PolicyEngine::new(policy).unwrap();
let request = RunRequest {
operation_id: "getUser".to_string(),
parameters: None,
body: None,
spec_path: None,
env: None,
auth_profile: Some("default".to_string()),
};
let context = EvaluationContext {
method: Some("GET".to_string()),
..Default::default()
};
let decision = engine.evaluate(&request, "https://api.example.com/users/123", &context);
assert!(decision.is_allowed());
assert_eq!(decision.rule_name(), "readonly");
}
#[test]
fn test_deny_write_operation() {
let policy = create_test_policy();
let engine = PolicyEngine::new(policy).unwrap();
let request = RunRequest {
operation_id: "createUser".to_string(),
parameters: None,
body: None,
spec_path: None,
env: None,
auth_profile: Some("default".to_string()),
};
let context = EvaluationContext {
method: Some("POST".to_string()),
..Default::default()
};
let decision = engine.evaluate(&request, "https://api.example.com/users", &context);
assert!(!decision.is_allowed());
}
#[test]
fn test_deny_no_auth() {
let policy = create_test_policy();
let engine = PolicyEngine::new(policy).unwrap();
let request = RunRequest {
operation_id: "getUser".to_string(),
parameters: None,
body: None,
spec_path: None,
env: None,
auth_profile: None, };
let context = EvaluationContext {
method: Some("GET".to_string()),
..Default::default()
};
let decision = engine.evaluate(&request, "https://api.example.com/users/123", &context);
assert!(!decision.is_allowed());
if let PolicyDecision::Deny { reason, .. } = decision {
assert!(reason.contains("Authentication required"));
}
}
#[test]
fn test_classify_operation_explicit() {
let mut policy = create_test_policy();
policy
.classifications
.insert("getPortfolio".to_string(), DataClassification::Confidential);
policy
.classifications
.insert("getStockPrice".to_string(), DataClassification::Public);
let engine = PolicyEngine::new(policy).unwrap();
assert_eq!(
engine.classify_operation("getPortfolio"),
DataClassification::Confidential
);
assert_eq!(
engine.classify_operation("getStockPrice"),
DataClassification::Public
);
}
#[test]
fn test_classify_operation_glob() {
let mut policy = create_test_policy();
policy
.classifications
.insert("*health*".to_string(), DataClassification::Public);
policy
.classifications
.insert("*admin*".to_string(), DataClassification::Regulated);
let engine = PolicyEngine::new(policy).unwrap();
assert_eq!(
engine.classify_operation("checkhealth"),
DataClassification::Public
);
assert_eq!(
engine.classify_operation("healthCheck"),
DataClassification::Public
);
assert_eq!(
engine.classify_operation("adminDeleteUser"),
DataClassification::Regulated
);
assert_eq!(
engine.classify_operation("checkHealth"),
DataClassification::Internal
);
}
#[test]
fn test_classify_operation_default() {
let policy = create_test_policy();
let engine = PolicyEngine::new(policy).unwrap();
assert_eq!(
engine.classify_operation("someRandomOp"),
DataClassification::Internal
);
}
fn create_scoped_policy(scopes: Vec<CredentialScope>) -> PolicySet {
let mut policy = create_test_policy();
policy.credential_scopes = scopes;
policy
}
#[test]
fn test_credential_scope_blocks_method() {
let policy = create_scoped_policy(vec![CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec!["GET".to_string()],
allowed_tags: vec![],
requires_approval: vec![],
}]);
let engine = PolicyEngine::new(policy).unwrap();
let result = engine.check_credential_scope("mcp", "POST", None);
assert!(result.is_some());
assert!(result.unwrap().contains("does not allow method POST"));
}
#[test]
fn test_credential_scope_allows_method() {
let policy = create_scoped_policy(vec![CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec!["GET".to_string()],
allowed_tags: vec![],
requires_approval: vec![],
}]);
let engine = PolicyEngine::new(policy).unwrap();
let result = engine.check_credential_scope("mcp", "GET", None);
assert!(result.is_none());
}
#[test]
fn test_credential_scope_no_match() {
let policy = create_scoped_policy(vec![CredentialScope {
profile: "other-profile".to_string(),
allowed_methods: vec!["GET".to_string()],
allowed_tags: vec![],
requires_approval: vec![],
}]);
let engine = PolicyEngine::new(policy).unwrap();
let result = engine.check_credential_scope("mcp", "DELETE", None);
assert!(result.is_none());
}
#[test]
fn test_credential_scope_requires_approval() {
let policy = create_scoped_policy(vec![CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec!["GET".to_string(), "POST".to_string(), "DELETE".to_string()],
allowed_tags: vec![],
requires_approval: vec!["DELETE".to_string()],
}]);
let engine = PolicyEngine::new(policy).unwrap();
let result = engine.check_credential_scope("mcp", "DELETE", None);
assert!(result.is_some());
assert!(result.unwrap().contains("requires human approval"));
}
#[test]
fn test_credential_scope_empty_methods_allows_all() {
let policy = create_scoped_policy(vec![CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec![], allowed_tags: vec![],
requires_approval: vec![],
}]);
let engine = PolicyEngine::new(policy).unwrap();
assert!(engine.check_credential_scope("mcp", "GET", None).is_none());
assert!(engine.check_credential_scope("mcp", "POST", None).is_none());
assert!(engine
.check_credential_scope("mcp", "DELETE", None)
.is_none());
}
#[test]
fn test_credential_scope_tag_filtering() {
let policy = create_scoped_policy(vec![CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec![],
allowed_tags: vec!["portfolio".to_string(), "stocks".to_string()],
requires_approval: vec![],
}]);
let engine = PolicyEngine::new(policy).unwrap();
let tags = vec!["portfolio".to_string()];
assert!(engine
.check_credential_scope("mcp", "GET", Some(&tags))
.is_none());
let tags = vec!["admin".to_string()];
let result = engine.check_credential_scope("mcp", "GET", Some(&tags));
assert!(result.is_some());
assert!(result.unwrap().contains("restricts access to tags"));
let result = engine.check_credential_scope("mcp", "GET", None);
assert!(result.is_some());
}
#[test]
fn test_credential_scope_duplicate_profiles_first_wins() {
let policy = create_scoped_policy(vec![
CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec!["GET".to_string()], allowed_tags: vec![],
requires_approval: vec![],
},
CredentialScope {
profile: "mcp".to_string(),
allowed_methods: vec!["GET".to_string(), "POST".to_string()], allowed_tags: vec![],
requires_approval: vec![],
},
]);
let engine = PolicyEngine::new(policy).unwrap();
let result = engine.check_credential_scope("mcp", "POST", None);
assert!(result.is_some());
assert!(result.unwrap().contains("does not allow method POST"));
}
#[test]
fn test_warn_confidential_accessor() {
let mut policy = create_test_policy();
policy.defaults.warn_on_confidential_to_llm = true;
let engine = PolicyEngine::new(policy).unwrap();
assert!(engine.warn_on_confidential());
let policy2 = create_test_policy();
let engine2 = PolicyEngine::new(policy2).unwrap();
assert!(!engine2.warn_on_confidential());
}
#[test]
fn test_block_regulated_accessor() {
let mut policy = create_test_policy();
policy.defaults.block_regulated_to_llm = true;
let engine = PolicyEngine::new(policy).unwrap();
assert!(engine.block_regulated());
let policy2 = create_test_policy();
let engine2 = PolicyEngine::new(policy2).unwrap();
assert!(!engine2.block_regulated());
}
}