Skip to main content

pmcp_code_mode/policy/
mod.rs

1//! Policy evaluation framework for Code Mode.
2//!
3//! This module provides a trait-based abstraction for policy evaluation,
4//! allowing different backends (AVP, local Cedar, etc.) to be used
5//! interchangeably.
6//!
7//! # Important
8//! The [`NoopPolicyEvaluator`] is provided for **testing and local development only**.
9//! Production servers MUST implement [`PolicyEvaluator`] with a real authorization backend.
10
11pub mod types;
12
13#[cfg(feature = "cedar")]
14pub mod cedar;
15
16pub use types::*;
17
18/// Error type for policy evaluation.
19#[derive(Debug, thiserror::Error)]
20pub enum PolicyEvaluationError {
21    #[error("Policy configuration error: {0}")]
22    ConfigError(String),
23
24    #[error("Policy evaluation error: {0}")]
25    EvaluationError(String),
26
27    #[error("Authorization denied: {0}")]
28    Denied(String),
29}
30
31/// Trait for policy evaluation backends.
32///
33/// Implementations can use different backends:
34/// - `AvpPolicyEvaluator` (in mcp-server-common): Uses AWS AVP
35/// - `CedarPolicyEvaluator` (in this crate): Uses local Cedar engine
36/// - Custom implementations for testing or other policy engines
37#[async_trait::async_trait]
38pub trait PolicyEvaluator: Send + Sync {
39    /// Evaluate a GraphQL operation against policies.
40    async fn evaluate_operation(
41        &self,
42        operation: &OperationEntity,
43        server_config: &ServerConfigEntity,
44    ) -> Result<AuthorizationDecision, PolicyEvaluationError>;
45
46    /// Evaluate a JavaScript script against policies (OpenAPI Code Mode).
47    /// Default: denies all scripts (override for OpenAPI support).
48    #[cfg(feature = "openapi-code-mode")]
49    async fn evaluate_script(
50        &self,
51        _script: &ScriptEntity,
52        _server: &OpenAPIServerEntity,
53    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
54        Ok(AuthorizationDecision {
55            allowed: false,
56            determining_policies: vec!["default_deny_scripts".to_string()],
57            errors: vec!["Script evaluation not supported by this policy evaluator".to_string()],
58        })
59    }
60
61    /// Batch evaluation (default: sequential).
62    async fn batch_evaluate(
63        &self,
64        requests: Vec<(OperationEntity, ServerConfigEntity)>,
65    ) -> Result<Vec<AuthorizationDecision>, PolicyEvaluationError> {
66        let mut results = Vec::with_capacity(requests.len());
67        for (op, config) in &requests {
68            results.push(self.evaluate_operation(op, config).await?);
69        }
70        Ok(results)
71    }
72
73    /// Whether this evaluator is configured and ready.
74    fn is_configured(&self) -> bool {
75        true
76    }
77
78    /// Human-readable name for logging.
79    fn name(&self) -> &str;
80}
81
82/// Always-allow policy evaluator for **testing and local development ONLY**.
83///
84/// # WARNING: Not for Production Use
85///
86/// Returns `allowed: true` for all policy evaluations, completely bypassing
87/// access control. Using this in production disables the entire policy layer.
88///
89/// For production, implement [`PolicyEvaluator`] with your authorization backend
90/// (e.g., `CedarPolicyEvaluator` behind the `cedar` feature, or a custom
91/// implementation calling AWS Verified Permissions).
92///
93/// # Example
94///
95/// ```rust
96/// use pmcp_code_mode::{NoopPolicyEvaluator, PolicyEvaluator};
97///
98/// // Test-only usage
99/// let evaluator = NoopPolicyEvaluator::new();
100/// assert_eq!(evaluator.name(), "noop");
101/// ```
102pub struct NoopPolicyEvaluator;
103
104impl NoopPolicyEvaluator {
105    /// Create a new no-op policy evaluator.
106    ///
107    /// # Warning
108    /// This evaluator allows ALL operations. Only use in tests or local development.
109    pub fn new() -> Self {
110        Self
111    }
112}
113
114impl Default for NoopPolicyEvaluator {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[async_trait::async_trait]
121impl PolicyEvaluator for NoopPolicyEvaluator {
122    async fn evaluate_operation(
123        &self,
124        _operation: &OperationEntity,
125        _server_config: &ServerConfigEntity,
126    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
127        Ok(AuthorizationDecision {
128            allowed: true,
129            determining_policies: vec!["noop_allow_all".to_string()],
130            errors: vec![],
131        })
132    }
133
134    #[cfg(feature = "openapi-code-mode")]
135    async fn evaluate_script(
136        &self,
137        _script: &ScriptEntity,
138        _server: &OpenAPIServerEntity,
139    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
140        Ok(AuthorizationDecision {
141            allowed: true,
142            determining_policies: vec!["noop_allow_all_scripts".to_string()],
143            errors: vec![],
144        })
145    }
146
147    fn name(&self) -> &str {
148        "noop"
149    }
150}
151
152#[cfg(test)]
153mod noop_tests {
154    use super::*;
155
156    #[tokio::test]
157    async fn noop_evaluator_allows_all_operations() {
158        let evaluator = NoopPolicyEvaluator::new();
159        let operation = OperationEntity {
160            id: "test-op".to_string(),
161            operation_type: "query".to_string(),
162            operation_name: "GetUsers".to_string(),
163            root_fields: ["users"].iter().map(|s| s.to_string()).collect(),
164            accessed_types: ["User"].iter().map(|s| s.to_string()).collect(),
165            accessed_fields: ["User.id", "User.name"]
166                .iter()
167                .map(|s| s.to_string())
168                .collect(),
169            depth: 2,
170            field_count: 2,
171            estimated_cost: 2,
172            has_introspection: false,
173            accesses_sensitive_data: false,
174            sensitive_categories: std::collections::HashSet::new(),
175        };
176        let config = ServerConfigEntity::default();
177        let result = evaluator
178            .evaluate_operation(&operation, &config)
179            .await
180            .unwrap();
181        assert!(result.allowed);
182        assert_eq!(result.determining_policies, vec!["noop_allow_all"]);
183    }
184
185    #[test]
186    fn noop_evaluator_name() {
187        let evaluator = NoopPolicyEvaluator::new();
188        assert_eq!(evaluator.name(), "noop");
189    }
190
191    #[test]
192    fn noop_evaluator_default() {
193        let evaluator = NoopPolicyEvaluator::default();
194        assert_eq!(evaluator.name(), "noop");
195    }
196}