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    /// Evaluate a SQL statement against policies (SQL Code Mode).
62    /// Default: denies all statements (override for SQL support).
63    #[cfg(feature = "sql-code-mode")]
64    async fn evaluate_statement(
65        &self,
66        _statement: &StatementEntity,
67        _server: &SqlServerEntity,
68    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
69        Ok(AuthorizationDecision {
70            allowed: false,
71            determining_policies: vec!["default_deny_statements".to_string()],
72            errors: vec![
73                "SQL statement evaluation not supported by this policy evaluator".to_string(),
74            ],
75        })
76    }
77
78    /// Batch evaluation (default: sequential).
79    async fn batch_evaluate(
80        &self,
81        requests: Vec<(OperationEntity, ServerConfigEntity)>,
82    ) -> Result<Vec<AuthorizationDecision>, PolicyEvaluationError> {
83        let mut results = Vec::with_capacity(requests.len());
84        for (op, config) in &requests {
85            results.push(self.evaluate_operation(op, config).await?);
86        }
87        Ok(results)
88    }
89
90    /// Whether this evaluator is configured and ready.
91    fn is_configured(&self) -> bool {
92        true
93    }
94
95    /// Human-readable name for logging.
96    fn name(&self) -> &str;
97}
98
99/// Always-allow policy evaluator for **testing and local development ONLY**.
100///
101/// # WARNING: Not for Production Use
102///
103/// Returns `allowed: true` for all policy evaluations, completely bypassing
104/// access control. Using this in production disables the entire policy layer.
105///
106/// For production, implement [`PolicyEvaluator`] with your authorization backend
107/// (e.g., `CedarPolicyEvaluator` behind the `cedar` feature, or a custom
108/// implementation calling AWS Verified Permissions).
109///
110/// # Example
111///
112/// ```rust
113/// use pmcp_code_mode::{NoopPolicyEvaluator, PolicyEvaluator};
114///
115/// // Test-only usage
116/// let evaluator = NoopPolicyEvaluator::new();
117/// assert_eq!(evaluator.name(), "noop");
118/// ```
119pub struct NoopPolicyEvaluator;
120
121impl NoopPolicyEvaluator {
122    /// Create a new no-op policy evaluator.
123    ///
124    /// # Warning
125    /// This evaluator allows ALL operations. Only use in tests or local development.
126    pub fn new() -> Self {
127        Self
128    }
129}
130
131impl Default for NoopPolicyEvaluator {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[async_trait::async_trait]
138impl PolicyEvaluator for NoopPolicyEvaluator {
139    async fn evaluate_operation(
140        &self,
141        _operation: &OperationEntity,
142        _server_config: &ServerConfigEntity,
143    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
144        Ok(AuthorizationDecision {
145            allowed: true,
146            determining_policies: vec!["noop_allow_all".to_string()],
147            errors: vec![],
148        })
149    }
150
151    #[cfg(feature = "openapi-code-mode")]
152    async fn evaluate_script(
153        &self,
154        _script: &ScriptEntity,
155        _server: &OpenAPIServerEntity,
156    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
157        Ok(AuthorizationDecision {
158            allowed: true,
159            determining_policies: vec!["noop_allow_all_scripts".to_string()],
160            errors: vec![],
161        })
162    }
163
164    #[cfg(feature = "sql-code-mode")]
165    async fn evaluate_statement(
166        &self,
167        _statement: &StatementEntity,
168        _server: &SqlServerEntity,
169    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
170        Ok(AuthorizationDecision {
171            allowed: true,
172            determining_policies: vec!["noop_allow_all_statements".to_string()],
173            errors: vec![],
174        })
175    }
176
177    fn name(&self) -> &str {
178        "noop"
179    }
180}
181
182#[cfg(test)]
183mod noop_tests {
184    use super::*;
185
186    #[tokio::test]
187    async fn noop_evaluator_allows_all_operations() {
188        let evaluator = NoopPolicyEvaluator::new();
189        let operation = OperationEntity {
190            id: "test-op".to_string(),
191            operation_type: "query".to_string(),
192            operation_name: "GetUsers".to_string(),
193            root_fields: ["users"].iter().map(|s| s.to_string()).collect(),
194            accessed_types: ["User"].iter().map(|s| s.to_string()).collect(),
195            accessed_fields: ["User.id", "User.name"]
196                .iter()
197                .map(|s| s.to_string())
198                .collect(),
199            depth: 2,
200            field_count: 2,
201            estimated_cost: 2,
202            has_introspection: false,
203            accesses_sensitive_data: false,
204            sensitive_categories: std::collections::HashSet::new(),
205        };
206        let config = ServerConfigEntity::default();
207        let result = evaluator
208            .evaluate_operation(&operation, &config)
209            .await
210            .unwrap();
211        assert!(result.allowed);
212        assert_eq!(result.determining_policies, vec!["noop_allow_all"]);
213    }
214
215    #[test]
216    fn noop_evaluator_name() {
217        let evaluator = NoopPolicyEvaluator::new();
218        assert_eq!(evaluator.name(), "noop");
219    }
220
221    #[test]
222    fn noop_evaluator_default() {
223        let evaluator = NoopPolicyEvaluator::default();
224        assert_eq!(evaluator.name(), "noop");
225    }
226}