Skip to main content

pmcp_code_mode/policy/
types.rs

1//! Domain types for policy evaluation.
2//!
3//! These types represent the entities used in Cedar policy evaluation.
4//! They are pure domain types with no AWS SDK dependency.
5
6use crate::graphql::GraphQLQueryInfo;
7use std::collections::HashSet;
8
9/// Server configuration for policy evaluation.
10///
11/// Uses unified attribute names that match the Cedar schema:
12/// - `allow_write`, `allow_delete`, `allow_admin` (unified action flags)
13/// - `blocked_operations`, `allowed_operations` (unified operation lists)
14#[derive(Debug, Clone)]
15pub struct ServerConfigEntity {
16    /// Server ID
17    pub server_id: String,
18
19    /// Server type (e.g., "graphql")
20    pub server_type: String,
21
22    /// Whether write operations (mutations) are allowed
23    pub allow_write: bool,
24
25    /// Whether delete operations are allowed
26    pub allow_delete: bool,
27
28    /// Whether admin operations (introspection) are allowed
29    pub allow_admin: bool,
30
31    /// Allowed operation names (allowlist mode)
32    pub allowed_operations: HashSet<String>,
33
34    /// Blocked operation names (blocklist mode)
35    pub blocked_operations: HashSet<String>,
36
37    /// Maximum query depth
38    pub max_depth: u32,
39
40    /// Maximum field count
41    pub max_field_count: u32,
42
43    /// Maximum estimated cost
44    pub max_cost: u32,
45
46    /// Maximum API calls (for compatibility with unified schema)
47    pub max_api_calls: u32,
48
49    /// Fields that should be blocked
50    pub blocked_fields: HashSet<String>,
51
52    /// Allowed sensitive data categories
53    pub allowed_sensitive_categories: HashSet<String>,
54}
55
56impl Default for ServerConfigEntity {
57    fn default() -> Self {
58        Self {
59            server_id: "unknown".to_string(),
60            server_type: "graphql".to_string(),
61            allow_write: false,
62            allow_delete: false,
63            allow_admin: false,
64            allowed_operations: HashSet::new(),
65            blocked_operations: HashSet::new(),
66            max_depth: 10,
67            max_field_count: 100,
68            max_cost: 1000,
69            max_api_calls: 50,
70            blocked_fields: HashSet::new(),
71            allowed_sensitive_categories: HashSet::new(),
72        }
73    }
74}
75
76/// Operation entity for policy evaluation.
77#[derive(Debug, Clone)]
78pub struct OperationEntity {
79    /// Unique ID for this operation
80    pub id: String,
81
82    /// Operation type: "query", "mutation", or "subscription"
83    pub operation_type: String,
84
85    /// Operation name (if provided)
86    pub operation_name: String,
87
88    /// Root fields accessed
89    pub root_fields: HashSet<String>,
90
91    /// Types accessed
92    pub accessed_types: HashSet<String>,
93
94    /// Fields accessed (Type.field format)
95    pub accessed_fields: HashSet<String>,
96
97    /// Query nesting depth
98    pub depth: u32,
99
100    /// Total field count
101    pub field_count: u32,
102
103    /// Estimated query cost
104    pub estimated_cost: u32,
105
106    /// Whether introspection is used
107    pub has_introspection: bool,
108
109    /// Whether sensitive data is accessed
110    pub accesses_sensitive_data: bool,
111
112    /// Sensitive data categories accessed
113    pub sensitive_categories: HashSet<String>,
114}
115
116impl OperationEntity {
117    /// Create from GraphQL query info.
118    pub fn from_query_info(query_info: &GraphQLQueryInfo) -> Self {
119        use crate::graphql::GraphQLOperationType;
120
121        let operation_type = match query_info.operation_type {
122            GraphQLOperationType::Query => "query",
123            GraphQLOperationType::Mutation => "mutation",
124            GraphQLOperationType::Subscription => "subscription",
125        };
126
127        Self {
128            id: query_info
129                .operation_name
130                .clone()
131                .unwrap_or_else(|| "anonymous".to_string()),
132            operation_type: operation_type.to_string(),
133            operation_name: query_info.operation_name.clone().unwrap_or_default(),
134            root_fields: query_info.root_fields.iter().cloned().collect(),
135            accessed_types: query_info.types_accessed.iter().cloned().collect(),
136            accessed_fields: query_info.fields_accessed.iter().cloned().collect(),
137            depth: query_info.max_depth as u32,
138            field_count: query_info.fields_accessed.len() as u32,
139            estimated_cost: query_info.fields_accessed.len() as u32,
140            has_introspection: query_info.has_introspection,
141            accesses_sensitive_data: false,
142            sensitive_categories: HashSet::new(),
143        }
144    }
145}
146
147/// Authorization decision from policy evaluation.
148#[derive(Debug, Clone)]
149pub struct AuthorizationDecision {
150    /// Whether the operation is allowed
151    pub allowed: bool,
152
153    /// Policy IDs that determined the decision
154    pub determining_policies: Vec<String>,
155
156    /// Error messages (if any)
157    pub errors: Vec<String>,
158}
159
160/// Script entity for policy evaluation (OpenAPI Code Mode).
161///
162/// Unlike GraphQL's single Operation entity, OpenAPI Code Mode validates
163/// JavaScript scripts that can contain multiple API calls with loops and logic.
164#[cfg(feature = "openapi-code-mode")]
165#[derive(Debug, Clone)]
166pub struct ScriptEntity {
167    /// Unique ID for this script validation
168    pub id: String,
169
170    /// Script type: "read_only", "mixed", or "write_only"
171    pub script_type: String,
172
173    /// Whether script contains any write operations (POST/PUT/PATCH/DELETE)
174    pub has_writes: bool,
175
176    /// Whether script contains DELETE operations
177    pub has_deletes: bool,
178
179    /// Total number of API calls in the script
180    pub total_api_calls: u32,
181
182    /// Number of GET calls
183    pub read_calls: u32,
184
185    /// Number of POST/PUT/PATCH calls
186    pub write_calls: u32,
187
188    /// Number of DELETE calls
189    pub delete_calls: u32,
190
191    /// Set of all paths accessed
192    pub accessed_paths: HashSet<String>,
193
194    /// Set of all HTTP methods used
195    pub accessed_methods: HashSet<String>,
196
197    /// Normalized path patterns (IDs replaced with *)
198    pub path_patterns: HashSet<String>,
199
200    /// Called operations in "METHOD:pathPattern" format for allowlist/blocklist matching
201    pub called_operations: HashSet<String>,
202
203    /// Maximum loop iterations (from .slice() bounds)
204    pub loop_iterations: u32,
205
206    /// Maximum nesting depth in the AST
207    pub nesting_depth: u32,
208
209    /// Script length in characters
210    pub script_length: u32,
211
212    /// Whether script accesses sensitive paths (/admin, /internal, etc.)
213    pub accesses_sensitive_path: bool,
214
215    /// Whether script has an unbounded loop
216    pub has_unbounded_loop: bool,
217
218    /// Whether script uses dynamic path interpolation
219    pub has_dynamic_path: bool,
220
221    /// Whether script has a @returns output declaration
222    pub has_output_declaration: bool,
223
224    /// Fields declared in the @returns annotation
225    pub output_fields: HashSet<String>,
226
227    /// Whether script uses spread operators in output (potential field leakage)
228    pub has_spread_in_output: bool,
229}
230
231#[cfg(feature = "openapi-code-mode")]
232impl ScriptEntity {
233    /// Build from JavaScript code analysis.
234    pub fn from_javascript_info(
235        info: &crate::javascript::JavaScriptCodeInfo,
236        sensitive_patterns: &[String],
237    ) -> Self {
238        use crate::javascript::HttpMethod;
239
240        let mut accessed_paths = HashSet::new();
241        let mut accessed_methods = HashSet::new();
242        let mut path_patterns = HashSet::new();
243        let mut called_operations = HashSet::new();
244        let mut read_calls = 0u32;
245        let mut write_calls = 0u32;
246        let mut delete_calls = 0u32;
247        let mut has_dynamic_path = false;
248        let mut accesses_sensitive_path = false;
249
250        for api_call in &info.api_calls {
251            accessed_paths.insert(api_call.path.clone());
252            let method_str = format!("{:?}", api_call.method).to_uppercase();
253            accessed_methods.insert(method_str.clone());
254
255            // Normalize path to pattern
256            let pattern = normalize_path_to_pattern(&api_call.path);
257            path_patterns.insert(pattern.clone());
258
259            // Build called operation string
260            called_operations.insert(format!("{}:{}", method_str, pattern));
261
262            // Count by method type
263            match api_call.method {
264                HttpMethod::Get | HttpMethod::Head | HttpMethod::Options => read_calls += 1,
265                HttpMethod::Delete => delete_calls += 1,
266                _ => write_calls += 1,
267            }
268
269            // Track dynamic paths
270            if api_call.is_dynamic_path {
271                has_dynamic_path = true;
272            }
273
274            // Check for sensitive path access
275            let path_lower = api_call.path.to_lowercase();
276            for pattern in sensitive_patterns {
277                if path_lower.contains(&pattern.to_lowercase()) {
278                    accesses_sensitive_path = true;
279                    break;
280                }
281            }
282        }
283
284        // Determine script type
285        let has_writes = write_calls > 0 || delete_calls > 0;
286        let has_reads = read_calls > 0;
287        let script_type = match (has_reads, has_writes) {
288            (true, false) => "read_only",
289            (false, true) => "write_only",
290            (true, true) => "mixed",
291            (false, false) => "empty",
292        };
293
294        Self {
295            id: info
296                .api_calls
297                .first()
298                .map(|c| format!("{}:{}", format!("{:?}", c.method).to_uppercase(), c.path))
299                .unwrap_or_else(|| "script".to_string()),
300            script_type: script_type.to_string(),
301            has_writes,
302            has_deletes: delete_calls > 0,
303            total_api_calls: info.api_calls.len() as u32,
304            read_calls,
305            write_calls,
306            delete_calls,
307            accessed_paths,
308            accessed_methods,
309            path_patterns,
310            called_operations,
311            loop_iterations: 0,
312            nesting_depth: info.max_depth as u32,
313            script_length: 0,
314            accesses_sensitive_path,
315            has_unbounded_loop: !info.all_loops_bounded && info.loop_count > 0,
316            has_dynamic_path,
317            has_output_declaration: info.output_declaration.has_declaration,
318            output_fields: info.output_declaration.declared_fields.clone(),
319            has_spread_in_output: info.output_declaration.has_spread_risk
320                || info.has_output_spread_risk,
321        }
322    }
323
324    /// Get the policy action for this script using unified action model.
325    pub fn action(&self) -> &'static str {
326        match self.script_type.as_str() {
327            "read_only" | "empty" => "Read",
328            "write_only" | "mixed" => {
329                if self.has_deletes {
330                    "Delete"
331                } else {
332                    "Write"
333                }
334            },
335            _ => "Read",
336        }
337    }
338}
339
340/// Check whether a path segment looks like a UUID (8-4-4-4-12 hex pattern).
341#[cfg(feature = "openapi-code-mode")]
342fn is_uuid_like(segment: &str) -> bool {
343    if segment.len() != 36 {
344        return false;
345    }
346    let parts: Vec<&str> = segment.split('-').collect();
347    matches!(parts.as_slice(), [a, b, c, d, e]
348        if a.len() == 8 && b.len() == 4 && c.len() == 4
349        && d.len() == 4 && e.len() == 12
350        && segment.chars().all(|ch| ch.is_ascii_hexdigit() || ch == '-'))
351}
352
353/// Normalize a path to a pattern by replacing numeric/UUID segments with *.
354#[cfg(feature = "openapi-code-mode")]
355pub fn normalize_path_to_pattern(path: &str) -> String {
356    path.split('/')
357        .map(|segment| {
358            if segment.chars().all(|c| c.is_ascii_digit()) || is_uuid_like(segment) {
359                "*"
360            } else {
361                segment
362            }
363        })
364        .collect::<Vec<_>>()
365        .join("/")
366}
367
368/// Normalize an operation string to the canonical "METHOD:/path" format.
369#[cfg(feature = "openapi-code-mode")]
370pub fn normalize_operation_format(op: &str) -> String {
371    let trimmed = op.trim();
372
373    let (method, path) = if let Some(idx) = trimmed.find(':') {
374        let potential_path = trimmed[idx + 1..].trim();
375        if potential_path.starts_with('/') {
376            let method = trimmed[..idx].trim();
377            (method, potential_path)
378        } else {
379            return trimmed.to_string();
380        }
381    } else if let Some(idx) = trimmed.find(' ') {
382        let method = trimmed[..idx].trim();
383        let path = trimmed[idx + 1..].trim();
384        (method, path)
385    } else {
386        return trimmed.to_string();
387    };
388
389    let method_upper = method.to_uppercase();
390
391    let normalized_path = path
392        .split('/')
393        .map(|segment| {
394            if segment.starts_with('{') && segment.ends_with('}') {
395                "*"
396            } else if segment.starts_with(':') {
397                "*"
398            } else if segment.chars().all(|c| c.is_ascii_digit()) {
399                "*"
400            } else if is_uuid_like(segment) {
401                "*"
402            } else {
403                segment
404            }
405        })
406        .collect::<Vec<_>>()
407        .join("/");
408
409    format!("{}:{}", method_upper, normalized_path)
410}
411
412/// Server configuration for OpenAPI Code Mode.
413#[cfg(feature = "openapi-code-mode")]
414#[derive(Debug, Clone)]
415pub struct OpenAPIServerEntity {
416    pub server_id: String,
417    pub server_type: String,
418
419    // Unified action flags
420    pub allow_write: bool,
421    pub allow_delete: bool,
422    pub allow_admin: bool,
423
424    // Write mode: "allow_all", "deny_all", "allowlist", "blocklist"
425    pub write_mode: String,
426
427    // Unified limits
428    pub max_depth: u32,
429    pub max_cost: u32,
430    pub max_api_calls: u32,
431
432    // OpenAPI-specific limits
433    pub max_loop_iterations: u32,
434    pub max_script_length: u32,
435    pub max_nesting_depth: u32,
436    pub execution_timeout_seconds: u32,
437
438    // Unified operation lists
439    pub allowed_operations: HashSet<String>,
440    pub blocked_operations: HashSet<String>,
441
442    // OpenAPI-specific method controls
443    pub allowed_methods: HashSet<String>,
444    pub blocked_methods: HashSet<String>,
445    pub allowed_path_patterns: HashSet<String>,
446    pub blocked_path_patterns: HashSet<String>,
447    pub sensitive_path_patterns: HashSet<String>,
448
449    // Auto-approval settings
450    pub auto_approve_read_only: bool,
451    pub max_api_calls_for_auto_approve: u32,
452
453    // Field control (two-tier blocklist)
454    pub internal_blocked_fields: HashSet<String>,
455    pub output_blocked_fields: HashSet<String>,
456    pub require_output_declaration: bool,
457}
458
459#[cfg(feature = "openapi-code-mode")]
460impl Default for OpenAPIServerEntity {
461    fn default() -> Self {
462        Self {
463            server_id: "unknown".to_string(),
464            server_type: "openapi".to_string(),
465            allow_write: false,
466            allow_delete: false,
467            allow_admin: false,
468            write_mode: "deny_all".to_string(),
469            max_depth: 10,
470            max_cost: 1000,
471            max_api_calls: 50,
472            max_loop_iterations: 100,
473            max_script_length: 10000,
474            max_nesting_depth: 10,
475            execution_timeout_seconds: 30,
476            allowed_operations: HashSet::new(),
477            blocked_operations: HashSet::new(),
478            allowed_methods: HashSet::new(),
479            blocked_methods: HashSet::new(),
480            allowed_path_patterns: HashSet::new(),
481            blocked_path_patterns: ["/admin".into(), "/internal".into()].into_iter().collect(),
482            sensitive_path_patterns: ["/admin".into(), "/internal".into(), "/debug".into()]
483                .into_iter()
484                .collect(),
485            auto_approve_read_only: true,
486            max_api_calls_for_auto_approve: 10,
487            internal_blocked_fields: HashSet::new(),
488            output_blocked_fields: HashSet::new(),
489            require_output_declaration: false,
490        }
491    }
492}
493
494/// Get the Cedar schema in JSON format.
495///
496/// Uses unified action model with Read/Write/Delete/Admin actions.
497pub fn get_code_mode_schema_json() -> serde_json::Value {
498    let applies_to = serde_json::json!({
499        "principalTypes": ["Operation"],
500        "resourceTypes": ["Server"],
501        "context": {
502            "type": "Record",
503            "attributes": {
504                "serverId": { "type": "String", "required": true },
505                "serverType": { "type": "String", "required": true },
506                "userId": { "type": "String", "required": false },
507                "sessionId": { "type": "String", "required": false }
508            }
509        }
510    });
511
512    serde_json::json!({
513        "CodeMode": {
514            "entityTypes": {
515                "Operation": {
516                    "shape": {
517                        "type": "Record",
518                        "attributes": {
519                            "operationType": { "type": "String", "required": true },
520                            "operationName": { "type": "String", "required": true },
521                            "rootFields": { "type": "Set", "element": { "type": "String" } },
522                            "accessedTypes": { "type": "Set", "element": { "type": "String" } },
523                            "accessedFields": { "type": "Set", "element": { "type": "String" } },
524                            "depth": { "type": "Long", "required": true },
525                            "fieldCount": { "type": "Long", "required": true },
526                            "estimatedCost": { "type": "Long", "required": true },
527                            "hasIntrospection": { "type": "Boolean", "required": true },
528                            "accessesSensitiveData": { "type": "Boolean", "required": true },
529                            "sensitiveCategories": { "type": "Set", "element": { "type": "String" } }
530                        }
531                    }
532                },
533                "Server": {
534                    "shape": {
535                        "type": "Record",
536                        "attributes": {
537                            "serverId": { "type": "String", "required": true },
538                            "serverType": { "type": "String", "required": true },
539                            "maxDepth": { "type": "Long", "required": true },
540                            "maxCost": { "type": "Long", "required": true },
541                            "maxApiCalls": { "type": "Long", "required": true },
542                            "allowWrite": { "type": "Boolean", "required": true },
543                            "allowDelete": { "type": "Boolean", "required": true },
544                            "allowAdmin": { "type": "Boolean", "required": true },
545                            "blockedOperations": { "type": "Set", "element": { "type": "String" } },
546                            "allowedOperations": { "type": "Set", "element": { "type": "String" } },
547                            "blockedFields": { "type": "Set", "element": { "type": "String" } }
548                        }
549                    }
550                }
551            },
552            "actions": {
553                "Read": { "appliesTo": applies_to },
554                "Write": { "appliesTo": applies_to },
555                "Delete": { "appliesTo": applies_to },
556                "Admin": { "appliesTo": applies_to }
557            }
558        }
559    })
560}
561
562/// Get the Cedar schema for OpenAPI Code Mode in JSON format.
563#[cfg(feature = "openapi-code-mode")]
564pub fn get_openapi_code_mode_schema_json() -> serde_json::Value {
565    let applies_to = serde_json::json!({
566        "principalTypes": ["Script"],
567        "resourceTypes": ["Server"],
568        "context": {
569            "type": "Record",
570            "attributes": {
571                "serverId": { "type": "String", "required": true },
572                "serverType": { "type": "String", "required": true },
573                "userId": { "type": "String", "required": false },
574                "sessionId": { "type": "String", "required": false }
575            }
576        }
577    });
578
579    serde_json::json!({
580        "CodeMode": {
581            "entityTypes": {
582                "Script": {
583                    "shape": {
584                        "type": "Record",
585                        "attributes": {
586                            "scriptType": { "type": "String", "required": true },
587                            "hasWrites": { "type": "Boolean", "required": true },
588                            "hasDeletes": { "type": "Boolean", "required": true },
589                            "totalApiCalls": { "type": "Long", "required": true },
590                            "readCalls": { "type": "Long", "required": true },
591                            "writeCalls": { "type": "Long", "required": true },
592                            "deleteCalls": { "type": "Long", "required": true },
593                            "accessedPaths": { "type": "Set", "element": { "type": "String" } },
594                            "accessedMethods": { "type": "Set", "element": { "type": "String" } },
595                            "pathPatterns": { "type": "Set", "element": { "type": "String" } },
596                            "calledOperations": { "type": "Set", "element": { "type": "String" } },
597                            "loopIterations": { "type": "Long", "required": true },
598                            "nestingDepth": { "type": "Long", "required": true },
599                            "scriptLength": { "type": "Long", "required": true },
600                            "accessesSensitivePath": { "type": "Boolean", "required": true },
601                            "hasUnboundedLoop": { "type": "Boolean", "required": true },
602                            "hasDynamicPath": { "type": "Boolean", "required": true },
603                            "outputFields": { "type": "Set", "element": { "type": "String" } },
604                            "hasOutputDeclaration": { "type": "Boolean", "required": true },
605                            "hasSpreadInOutput": { "type": "Boolean", "required": true }
606                        }
607                    }
608                },
609                "Server": {
610                    "shape": {
611                        "type": "Record",
612                        "attributes": {
613                            "serverId": { "type": "String", "required": true },
614                            "serverType": { "type": "String", "required": true },
615                            "maxDepth": { "type": "Long", "required": true },
616                            "maxCost": { "type": "Long", "required": true },
617                            "maxApiCalls": { "type": "Long", "required": true },
618                            "allowWrite": { "type": "Boolean", "required": true },
619                            "allowDelete": { "type": "Boolean", "required": true },
620                            "allowAdmin": { "type": "Boolean", "required": true },
621                            "blockedOperations": { "type": "Set", "element": { "type": "String" } },
622                            "allowedOperations": { "type": "Set", "element": { "type": "String" } },
623                            "blockedFields": { "type": "Set", "element": { "type": "String" } },
624                            "maxLoopIterations": { "type": "Long", "required": true },
625                            "maxScriptLength": { "type": "Long", "required": true },
626                            "maxNestingDepth": { "type": "Long", "required": true },
627                            "executionTimeoutSeconds": { "type": "Long", "required": true },
628                            "allowedMethods": { "type": "Set", "element": { "type": "String" } },
629                            "blockedMethods": { "type": "Set", "element": { "type": "String" } },
630                            "allowedPathPatterns": { "type": "Set", "element": { "type": "String" } },
631                            "blockedPathPatterns": { "type": "Set", "element": { "type": "String" } },
632                            "sensitivePathPatterns": { "type": "Set", "element": { "type": "String" } },
633                            "autoApproveReadOnly": { "type": "Boolean", "required": true },
634                            "maxApiCallsForAutoApprove": { "type": "Long", "required": true },
635                            "internalBlockedFields": { "type": "Set", "element": { "type": "String" } },
636                            "outputBlockedFields": { "type": "Set", "element": { "type": "String" } },
637                            "requireOutputDeclaration": { "type": "Boolean", "required": true }
638                        }
639                    }
640                }
641            },
642            "actions": {
643                "Read": { "appliesTo": applies_to },
644                "Write": { "appliesTo": applies_to },
645                "Delete": { "appliesTo": applies_to },
646                "Admin": { "appliesTo": applies_to }
647            }
648        }
649    })
650}
651
652/// Get baseline Cedar policies for OpenAPI Code Mode.
653#[cfg(feature = "openapi-code-mode")]
654pub fn get_openapi_baseline_policies() -> Vec<(&'static str, &'static str, &'static str)> {
655    vec![
656        (
657            "permit_reads",
658            "Permit all read operations (GET scripts)",
659            r#"permit(principal, action == CodeMode::Action::"Read", resource);"#,
660        ),
661        (
662            "permit_writes",
663            "Permit write operations (when enabled)",
664            r#"permit(principal, action == CodeMode::Action::"Write", resource) when { resource.allowWrite == true };"#,
665        ),
666        (
667            "permit_deletes",
668            "Permit delete operations (when enabled)",
669            r#"permit(principal, action == CodeMode::Action::"Delete", resource) when { resource.allowDelete == true };"#,
670        ),
671        (
672            "forbid_sensitive_paths",
673            "Block scripts accessing sensitive paths",
674            r#"forbid(principal, action, resource) when { principal.accessesSensitivePath == true };"#,
675        ),
676        (
677            "forbid_unbounded_loops",
678            "Block scripts with unbounded loops",
679            r#"forbid(principal, action, resource) when { principal.hasUnboundedLoop == true };"#,
680        ),
681        (
682            "forbid_excessive_api_calls",
683            "Enforce API call limit",
684            r#"forbid(principal, action, resource) when { principal.totalApiCalls > resource.maxApiCalls };"#,
685        ),
686        (
687            "forbid_excessive_nesting",
688            "Enforce nesting depth limit",
689            r#"forbid(principal, action, resource) when { principal.nestingDepth > resource.maxNestingDepth };"#,
690        ),
691        (
692            "forbid_output_blocked_fields",
693            "Block scripts that return output-blocked fields",
694            r#"forbid(principal, action, resource) when { principal.outputFields.containsAny(resource.outputBlockedFields) };"#,
695        ),
696        (
697            "forbid_spread_without_declaration",
698            "Block scripts with spread in output when output declaration is required",
699            r#"forbid(principal, action, resource) when { principal.hasSpreadInOutput == true && resource.requireOutputDeclaration == true };"#,
700        ),
701        (
702            "forbid_missing_output_declaration",
703            "Block scripts without output declaration when required",
704            r#"forbid(principal, action, resource) when { principal.hasOutputDeclaration == false && resource.requireOutputDeclaration == true };"#,
705        ),
706    ]
707}
708
709/// Get the baseline Cedar policies.
710pub fn get_baseline_policies() -> Vec<(&'static str, &'static str, &'static str)> {
711    vec![
712        (
713            "permit_reads",
714            "Permit all read operations (queries)",
715            r#"permit(principal, action == CodeMode::Action::"Read", resource);"#,
716        ),
717        (
718            "permit_writes",
719            "Permit write operations (when enabled)",
720            r#"permit(principal, action == CodeMode::Action::"Write", resource) when { resource.allowWrite == true };"#,
721        ),
722        (
723            "permit_deletes",
724            "Permit delete operations (when enabled)",
725            r#"permit(principal, action == CodeMode::Action::"Delete", resource) when { resource.allowDelete == true };"#,
726        ),
727        (
728            "permit_admin",
729            "Permit admin operations (when enabled)",
730            r#"permit(principal, action == CodeMode::Action::"Admin", resource) when { resource.allowAdmin == true };"#,
731        ),
732        (
733            "forbid_blocked_operations",
734            "Block operations in blocklist",
735            r#"forbid(principal, action, resource) when { resource.blockedOperations.contains(principal.operationName) };"#,
736        ),
737        (
738            "forbid_blocked_fields",
739            "Block access to blocked fields",
740            r#"forbid(principal, action, resource) when { resource.blockedFields.containsAny(principal.accessedFields) };"#,
741        ),
742        (
743            "forbid_excessive_depth",
744            "Enforce maximum query depth",
745            r#"forbid(principal, action, resource) when { principal.depth > resource.maxDepth };"#,
746        ),
747        (
748            "forbid_excessive_cost",
749            "Enforce maximum query cost",
750            r#"forbid(principal, action, resource) when { principal.estimatedCost > resource.maxCost };"#,
751        ),
752    ]
753}