Skip to main content

pmcp_code_mode/
avp.rs

1//! Amazon Verified Permissions (AVP) policy evaluator for Code Mode.
2//!
3//! Provides [`AvpPolicyEvaluator`] — an implementation of [`PolicyEvaluator`] backed by
4//! AWS Verified Permissions. Supports both GraphQL (`evaluate_operation`) and JavaScript
5//! (`evaluate_script`) policy evaluation.
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use pmcp_code_mode::{AvpClient, AvpConfig, AvpPolicyEvaluator};
11//! use std::sync::Arc;
12//!
13//! // Construct from POLICY_STORE_ID env var (injected by pmcp.run platform)
14//! let config = AvpConfig {
15//!     policy_store_id: std::env::var("POLICY_STORE_ID").unwrap(),
16//!     region: None, // uses default AWS region
17//! };
18//! let client = AvpClient::new(config).await?;
19//! let evaluator = Arc::new(AvpPolicyEvaluator::new(client));
20//! ```
21//!
22//! # Feature Gate
23//!
24//! This module requires the `avp` feature:
25//! ```toml
26//! pmcp-code-mode = { version = "0.4.0", features = ["avp"] }
27//! ```
28
29use aws_config::BehaviorVersion;
30use aws_sdk_verifiedpermissions::{
31    types::{ActionIdentifier, AttributeValue, EntitiesDefinition, EntityIdentifier, EntityItem},
32    Client,
33};
34use serde::{Deserialize, Serialize};
35use std::collections::{HashMap, HashSet};
36
37use crate::policy::{
38    AuthorizationDecision, OperationEntity, PolicyEvaluationError, PolicyEvaluator,
39    ServerConfigEntity,
40};
41
42#[cfg(feature = "openapi-code-mode")]
43use crate::policy::{normalize_operation_format, OpenAPIServerEntity, ScriptEntity};
44
45#[cfg(feature = "sql-code-mode")]
46use crate::policy::{SqlServerEntity, StatementEntity};
47
48/// Configuration for the AVP client.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct AvpConfig {
51    /// The AVP Policy Store ID for this server.
52    pub policy_store_id: String,
53
54    /// AWS region (optional, uses default if not set).
55    #[serde(default)]
56    pub region: Option<String>,
57}
58
59impl Default for AvpConfig {
60    fn default() -> Self {
61        Self {
62            policy_store_id: String::new(),
63            region: None,
64        }
65    }
66}
67
68/// Error type for AVP operations.
69#[derive(Debug, thiserror::Error)]
70pub enum AvpError {
71    #[error("AVP configuration error: {0}")]
72    ConfigError(String),
73
74    #[error("AVP SDK error: {0}")]
75    SdkError(String),
76
77    #[error("Authorization denied: {0}")]
78    Denied(String),
79}
80
81/// AVP client for Code Mode policy evaluation.
82///
83/// Wraps the AWS SDK `verifiedpermissions::Client` and provides typed methods
84/// for authorizing GraphQL operations and JavaScript scripts against Cedar
85/// policies managed in AWS Verified Permissions.
86#[derive(Clone)]
87pub struct AvpClient {
88    client: Client,
89    policy_store_id: String,
90}
91
92impl AvpClient {
93    /// Create a new AVP client.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`AvpError::ConfigError`] if the policy store ID is empty.
98    pub async fn new(config: AvpConfig) -> Result<Self, AvpError> {
99        if config.policy_store_id.is_empty() {
100            return Err(AvpError::ConfigError(
101                "Policy store ID is required".to_string(),
102            ));
103        }
104
105        let aws_config = if let Some(region) = &config.region {
106            aws_config::defaults(BehaviorVersion::latest())
107                .region(aws_config::Region::new(region.clone()))
108                .load()
109                .await
110        } else {
111            aws_config::load_defaults(BehaviorVersion::latest()).await
112        };
113
114        let client = Client::new(&aws_config);
115
116        Ok(Self {
117            client,
118            policy_store_id: config.policy_store_id,
119        })
120    }
121
122    /// Check if a GraphQL operation is authorized.
123    pub async fn is_authorized(
124        &self,
125        operation: &OperationEntity,
126        server_config: &ServerConfigEntity,
127    ) -> Result<AuthorizationDecision, AvpError> {
128        let entities = self.build_entities(operation, server_config);
129
130        let action_id = if operation.has_introspection {
131            "Admin"
132        } else {
133            match operation.operation_type.as_str() {
134                "mutation" => {
135                    let op_name = operation.operation_name.to_lowercase();
136                    if op_name.starts_with("delete")
137                        || op_name.starts_with("remove")
138                        || op_name.starts_with("purge")
139                    {
140                        "Delete"
141                    } else {
142                        "Write"
143                    }
144                },
145                "subscription" => "Write",
146                _ => "Read",
147            }
148        };
149
150        let response = self
151            .client
152            .is_authorized()
153            .policy_store_id(&self.policy_store_id)
154            .principal(
155                EntityIdentifier::builder()
156                    .entity_type("CodeMode::Operation")
157                    .entity_id(&operation.id)
158                    .build()
159                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
160            )
161            .action(
162                ActionIdentifier::builder()
163                    .action_type("CodeMode::Action")
164                    .action_id(action_id)
165                    .build()
166                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
167            )
168            .resource(
169                EntityIdentifier::builder()
170                    .entity_type("CodeMode::Server")
171                    .entity_id(&server_config.server_id)
172                    .build()
173                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
174            )
175            .entities(entities)
176            .send()
177            .await
178            .map_err(|e| {
179                tracing::error!(error = ?e, "AVP is_authorized failed");
180                AvpError::SdkError(e.to_string())
181            })?;
182
183        Ok(self.parse_response(&response))
184    }
185
186    /// Generic authorization check using raw entity types and attributes.
187    ///
188    /// Use this when your server type has a unique Cedar schema that doesn't
189    /// match the typed entity structs (`OperationEntity`, `ScriptEntity`, etc.).
190    pub async fn is_authorized_raw(
191        &self,
192        principal_type: &str,
193        principal_id: &str,
194        action_type: &str,
195        action_id: &str,
196        resource_type: &str,
197        resource_id: &str,
198        entities: Vec<EntityItem>,
199    ) -> Result<AuthorizationDecision, AvpError> {
200        let response = self
201            .client
202            .is_authorized()
203            .policy_store_id(&self.policy_store_id)
204            .principal(
205                EntityIdentifier::builder()
206                    .entity_type(principal_type)
207                    .entity_id(principal_id)
208                    .build()
209                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
210            )
211            .action(
212                ActionIdentifier::builder()
213                    .action_type(action_type)
214                    .action_id(action_id)
215                    .build()
216                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
217            )
218            .resource(
219                EntityIdentifier::builder()
220                    .entity_type(resource_type)
221                    .entity_id(resource_id)
222                    .build()
223                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
224            )
225            .entities(EntitiesDefinition::EntityList(entities))
226            .send()
227            .await
228            .map_err(|e| {
229                tracing::error!(error = ?e, "AVP is_authorized failed");
230                AvpError::SdkError(e.to_string())
231            })?;
232
233        Ok(self.parse_response(&response))
234    }
235
236    /// Batch authorization for multiple operations (chunks of 30 per API limit).
237    pub async fn batch_is_authorized(
238        &self,
239        requests: Vec<(OperationEntity, ServerConfigEntity)>,
240    ) -> Result<Vec<AuthorizationDecision>, AvpError> {
241        let mut results = Vec::new();
242
243        for chunk in requests.chunks(30) {
244            let batch_items: Vec<_> = chunk
245                .iter()
246                .map(|(op, config)| {
247                    let action_id = Self::determine_action_id(op);
248
249                    aws_sdk_verifiedpermissions::types::BatchIsAuthorizedInputItem::builder()
250                        .principal(
251                            EntityIdentifier::builder()
252                                .entity_type("CodeMode::Operation")
253                                .entity_id(&op.id)
254                                .build()
255                                .expect("valid entity identifier"),
256                        )
257                        .action(
258                            ActionIdentifier::builder()
259                                .action_type("CodeMode::Action")
260                                .action_id(action_id)
261                                .build()
262                                .expect("valid action identifier"),
263                        )
264                        .resource(
265                            EntityIdentifier::builder()
266                                .entity_type("CodeMode::Server")
267                                .entity_id(&config.server_id)
268                                .build()
269                                .expect("valid entity identifier"),
270                        )
271                        .build()
272                })
273                .collect();
274
275            let mut all_entities = Vec::new();
276            for (op, config) in chunk {
277                all_entities.push(self.build_operation_entity(op));
278                all_entities.push(self.build_server_config_entity(config));
279            }
280
281            let response = self
282                .client
283                .batch_is_authorized()
284                .policy_store_id(&self.policy_store_id)
285                .set_requests(Some(batch_items))
286                .entities(EntitiesDefinition::EntityList(all_entities))
287                .send()
288                .await
289                .map_err(|e| {
290                    tracing::error!(error = ?e, "AVP is_authorized failed");
291                    AvpError::SdkError(e.to_string())
292                })?;
293
294            for result in response.results() {
295                let allowed =
296                    result.decision() == &aws_sdk_verifiedpermissions::types::Decision::Allow;
297                results.push(AuthorizationDecision {
298                    allowed,
299                    determining_policies: result
300                        .determining_policies()
301                        .iter()
302                        .map(|p| p.policy_id().to_string())
303                        .collect(),
304                    errors: result
305                        .errors()
306                        .iter()
307                        .map(|e| e.error_description().to_string())
308                        .collect(),
309                });
310            }
311        }
312
313        Ok(results)
314    }
315
316    fn determine_action_id(op: &OperationEntity) -> &'static str {
317        if op.has_introspection {
318            "Admin"
319        } else {
320            match op.operation_type.as_str() {
321                "mutation" => {
322                    let op_name = op.operation_name.to_lowercase();
323                    if op_name.starts_with("delete")
324                        || op_name.starts_with("remove")
325                        || op_name.starts_with("purge")
326                    {
327                        "Delete"
328                    } else {
329                        "Write"
330                    }
331                },
332                "subscription" => "Write",
333                _ => "Read",
334            }
335        }
336    }
337
338    fn parse_response(
339        &self,
340        response: &aws_sdk_verifiedpermissions::operation::is_authorized::IsAuthorizedOutput,
341    ) -> AuthorizationDecision {
342        let allowed = response.decision() == &aws_sdk_verifiedpermissions::types::Decision::Allow;
343        AuthorizationDecision {
344            allowed,
345            determining_policies: response
346                .determining_policies()
347                .iter()
348                .map(|p| p.policy_id().to_string())
349                .collect(),
350            errors: response
351                .errors()
352                .iter()
353                .map(|e| e.error_description().to_string())
354                .collect(),
355        }
356    }
357
358    fn build_entities(
359        &self,
360        operation: &OperationEntity,
361        server_config: &ServerConfigEntity,
362    ) -> EntitiesDefinition {
363        EntitiesDefinition::EntityList(vec![
364            self.build_operation_entity(operation),
365            self.build_server_config_entity(server_config),
366        ])
367    }
368
369    fn build_operation_entity(&self, operation: &OperationEntity) -> EntityItem {
370        let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
371        attrs.insert(
372            "operationType".into(),
373            AttributeValue::String(operation.operation_type.clone()),
374        );
375        attrs.insert(
376            "operationName".into(),
377            AttributeValue::String(operation.operation_name.clone()),
378        );
379        attrs.insert("depth".into(), AttributeValue::Long(operation.depth as i64));
380        attrs.insert(
381            "fieldCount".into(),
382            AttributeValue::Long(operation.field_count as i64),
383        );
384        attrs.insert(
385            "estimatedCost".into(),
386            AttributeValue::Long(operation.estimated_cost as i64),
387        );
388        attrs.insert(
389            "hasIntrospection".into(),
390            AttributeValue::Boolean(operation.has_introspection),
391        );
392        attrs.insert(
393            "accessesSensitiveData".into(),
394            AttributeValue::Boolean(operation.accesses_sensitive_data),
395        );
396        attrs.insert(
397            "rootFields".into(),
398            Self::string_set(&operation.root_fields),
399        );
400        attrs.insert(
401            "accessedTypes".into(),
402            Self::string_set(&operation.accessed_types),
403        );
404        attrs.insert(
405            "accessedFields".into(),
406            Self::string_set(&operation.accessed_fields),
407        );
408        attrs.insert(
409            "sensitiveCategories".into(),
410            Self::string_set(&operation.sensitive_categories),
411        );
412
413        EntityItem::builder()
414            .identifier(
415                EntityIdentifier::builder()
416                    .entity_type("CodeMode::Operation")
417                    .entity_id(&operation.id)
418                    .build()
419                    .expect("valid entity identifier"),
420            )
421            .set_attributes(Some(attrs))
422            .build()
423    }
424
425    fn build_server_config_entity(&self, config: &ServerConfigEntity) -> EntityItem {
426        let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
427        attrs.insert(
428            "serverId".into(),
429            AttributeValue::String(config.server_id.clone()),
430        );
431        attrs.insert(
432            "serverType".into(),
433            AttributeValue::String(config.server_type.clone()),
434        );
435        attrs.insert(
436            "allowWrite".into(),
437            AttributeValue::Boolean(config.allow_write),
438        );
439        attrs.insert(
440            "allowDelete".into(),
441            AttributeValue::Boolean(config.allow_delete),
442        );
443        attrs.insert(
444            "allowAdmin".into(),
445            AttributeValue::Boolean(config.allow_admin),
446        );
447        attrs.insert(
448            "maxDepth".into(),
449            AttributeValue::Long(config.max_depth as i64),
450        );
451        attrs.insert(
452            "maxFieldCount".into(),
453            AttributeValue::Long(config.max_field_count as i64),
454        );
455        attrs.insert(
456            "maxCost".into(),
457            AttributeValue::Long(config.max_cost as i64),
458        );
459        attrs.insert(
460            "maxApiCalls".into(),
461            AttributeValue::Long(config.max_api_calls as i64),
462        );
463        attrs.insert(
464            "allowedOperations".into(),
465            Self::string_set(&config.allowed_operations),
466        );
467        attrs.insert(
468            "blockedOperations".into(),
469            Self::string_set(&config.blocked_operations),
470        );
471        attrs.insert(
472            "blockedFields".into(),
473            Self::string_set(&config.blocked_fields),
474        );
475
476        EntityItem::builder()
477            .identifier(
478                EntityIdentifier::builder()
479                    .entity_type("CodeMode::Server")
480                    .entity_id(&config.server_id)
481                    .build()
482                    .expect("valid entity identifier"),
483            )
484            .set_attributes(Some(attrs))
485            .build()
486    }
487
488    fn string_set(set: &HashSet<String>) -> AttributeValue {
489        AttributeValue::Set(
490            set.iter()
491                .map(|s| AttributeValue::String(s.clone()))
492                .collect(),
493        )
494    }
495}
496
497// ============================================================================
498// OpenAPI Code Mode Support (Script-based validation)
499// ============================================================================
500
501#[cfg(feature = "openapi-code-mode")]
502impl AvpClient {
503    /// Check if a JavaScript script is authorized (OpenAPI Code Mode).
504    pub async fn is_script_authorized(
505        &self,
506        script: &ScriptEntity,
507        server: &OpenAPIServerEntity,
508    ) -> Result<AuthorizationDecision, AvpError> {
509        let entities = EntitiesDefinition::EntityList(vec![
510            self.build_script_entity(script),
511            self.build_openapi_server_entity(server),
512        ]);
513
514        let response = self
515            .client
516            .is_authorized()
517            .policy_store_id(&self.policy_store_id)
518            .principal(
519                EntityIdentifier::builder()
520                    .entity_type("CodeMode::Script")
521                    .entity_id(&script.id)
522                    .build()
523                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
524            )
525            .action(
526                ActionIdentifier::builder()
527                    .action_type("CodeMode::Action")
528                    .action_id(script.action())
529                    .build()
530                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
531            )
532            .resource(
533                EntityIdentifier::builder()
534                    .entity_type("CodeMode::Server")
535                    .entity_id(&server.server_id)
536                    .build()
537                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
538            )
539            .entities(entities)
540            .send()
541            .await
542            .map_err(|e| {
543                tracing::error!(error = ?e, "AVP is_authorized failed");
544                AvpError::SdkError(e.to_string())
545            })?;
546
547        Ok(self.parse_response(&response))
548    }
549
550    fn build_script_entity(&self, script: &ScriptEntity) -> EntityItem {
551        let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
552        attrs.insert(
553            "scriptType".into(),
554            AttributeValue::String(script.script_type.clone()),
555        );
556        attrs.insert(
557            "hasWrites".into(),
558            AttributeValue::Boolean(script.has_writes),
559        );
560        attrs.insert(
561            "hasDeletes".into(),
562            AttributeValue::Boolean(script.has_deletes),
563        );
564        attrs.insert(
565            "accessesSensitivePath".into(),
566            AttributeValue::Boolean(script.accesses_sensitive_path),
567        );
568        attrs.insert(
569            "hasUnboundedLoop".into(),
570            AttributeValue::Boolean(script.has_unbounded_loop),
571        );
572        attrs.insert(
573            "hasDynamicPath".into(),
574            AttributeValue::Boolean(script.has_dynamic_path),
575        );
576        attrs.insert(
577            "totalApiCalls".into(),
578            AttributeValue::Long(script.total_api_calls as i64),
579        );
580        attrs.insert(
581            "readCalls".into(),
582            AttributeValue::Long(script.read_calls as i64),
583        );
584        attrs.insert(
585            "writeCalls".into(),
586            AttributeValue::Long(script.write_calls as i64),
587        );
588        attrs.insert(
589            "deleteCalls".into(),
590            AttributeValue::Long(script.delete_calls as i64),
591        );
592        attrs.insert(
593            "loopIterations".into(),
594            AttributeValue::Long(script.loop_iterations as i64),
595        );
596        attrs.insert(
597            "nestingDepth".into(),
598            AttributeValue::Long(script.nesting_depth as i64),
599        );
600        attrs.insert(
601            "scriptLength".into(),
602            AttributeValue::Long(script.script_length as i64),
603        );
604        attrs.insert(
605            "accessedPaths".into(),
606            Self::string_set(&script.accessed_paths),
607        );
608        attrs.insert(
609            "accessedMethods".into(),
610            Self::string_set(&script.accessed_methods),
611        );
612        attrs.insert(
613            "pathPatterns".into(),
614            Self::string_set(&script.path_patterns),
615        );
616        attrs.insert(
617            "calledOperations".into(),
618            Self::string_set(&script.called_operations),
619        );
620        attrs.insert(
621            "hasOutputDeclaration".into(),
622            AttributeValue::Boolean(script.has_output_declaration),
623        );
624        attrs.insert(
625            "outputFields".into(),
626            Self::string_set(&script.output_fields),
627        );
628        attrs.insert(
629            "hasSpreadInOutput".into(),
630            AttributeValue::Boolean(script.has_spread_in_output),
631        );
632
633        EntityItem::builder()
634            .identifier(
635                EntityIdentifier::builder()
636                    .entity_type("CodeMode::Script")
637                    .entity_id(&script.id)
638                    .build()
639                    .expect("valid entity identifier"),
640            )
641            .set_attributes(Some(attrs))
642            .build()
643    }
644
645    fn build_openapi_server_entity(&self, server: &OpenAPIServerEntity) -> EntityItem {
646        let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
647        attrs.insert(
648            "serverId".into(),
649            AttributeValue::String(server.server_id.clone()),
650        );
651        attrs.insert(
652            "serverType".into(),
653            AttributeValue::String(server.server_type.clone()),
654        );
655        attrs.insert(
656            "allowWrite".into(),
657            AttributeValue::Boolean(server.allow_write),
658        );
659        attrs.insert(
660            "allowDelete".into(),
661            AttributeValue::Boolean(server.allow_delete),
662        );
663        attrs.insert(
664            "allowAdmin".into(),
665            AttributeValue::Boolean(server.allow_admin),
666        );
667        attrs.insert(
668            "writeMode".into(),
669            AttributeValue::String(server.write_mode.clone()),
670        );
671        attrs.insert(
672            "maxDepth".into(),
673            AttributeValue::Long(server.max_depth as i64),
674        );
675        attrs.insert(
676            "maxCost".into(),
677            AttributeValue::Long(server.max_cost as i64),
678        );
679        attrs.insert(
680            "maxApiCalls".into(),
681            AttributeValue::Long(server.max_api_calls as i64),
682        );
683        attrs.insert(
684            "maxLoopIterations".into(),
685            AttributeValue::Long(server.max_loop_iterations as i64),
686        );
687        attrs.insert(
688            "maxScriptLength".into(),
689            AttributeValue::Long(server.max_script_length as i64),
690        );
691        attrs.insert(
692            "maxNestingDepth".into(),
693            AttributeValue::Long(server.max_nesting_depth as i64),
694        );
695        attrs.insert(
696            "executionTimeoutSeconds".into(),
697            AttributeValue::Long(server.execution_timeout_seconds as i64),
698        );
699        attrs.insert(
700            "allowedOperations".into(),
701            AttributeValue::Set(
702                server
703                    .allowed_operations
704                    .iter()
705                    .map(|s| AttributeValue::String(normalize_operation_format(s)))
706                    .collect(),
707            ),
708        );
709        attrs.insert(
710            "blockedOperations".into(),
711            AttributeValue::Set(
712                server
713                    .blocked_operations
714                    .iter()
715                    .map(|s| AttributeValue::String(normalize_operation_format(s)))
716                    .collect(),
717            ),
718        );
719        attrs.insert(
720            "allowedMethods".into(),
721            Self::string_set(&server.allowed_methods),
722        );
723        attrs.insert(
724            "blockedMethods".into(),
725            Self::string_set(&server.blocked_methods),
726        );
727        attrs.insert(
728            "allowedPathPatterns".into(),
729            Self::string_set(&server.allowed_path_patterns),
730        );
731        attrs.insert(
732            "blockedPathPatterns".into(),
733            Self::string_set(&server.blocked_path_patterns),
734        );
735        attrs.insert(
736            "sensitivePathPatterns".into(),
737            Self::string_set(&server.sensitive_path_patterns),
738        );
739        attrs.insert(
740            "autoApproveReadOnly".into(),
741            AttributeValue::Boolean(server.auto_approve_read_only),
742        );
743        attrs.insert(
744            "maxApiCallsForAutoApprove".into(),
745            AttributeValue::Long(server.max_api_calls_for_auto_approve as i64),
746        );
747        attrs.insert(
748            "internalBlockedFields".into(),
749            Self::string_set(&server.internal_blocked_fields),
750        );
751        attrs.insert(
752            "outputBlockedFields".into(),
753            Self::string_set(&server.output_blocked_fields),
754        );
755        attrs.insert(
756            "requireOutputDeclaration".into(),
757            AttributeValue::Boolean(server.require_output_declaration),
758        );
759
760        EntityItem::builder()
761            .identifier(
762                EntityIdentifier::builder()
763                    .entity_type("CodeMode::Server")
764                    .entity_id(&server.server_id)
765                    .build()
766                    .expect("valid entity identifier"),
767            )
768            .set_attributes(Some(attrs))
769            .build()
770    }
771}
772
773// ============================================================================
774// SQL Code Mode Support (Statement-based validation)
775// ============================================================================
776
777#[cfg(feature = "sql-code-mode")]
778impl AvpClient {
779    /// Check if a SQL statement is authorized (SQL Code Mode).
780    pub async fn is_statement_authorized(
781        &self,
782        statement: &StatementEntity,
783        server: &SqlServerEntity,
784    ) -> Result<AuthorizationDecision, AvpError> {
785        let entities = EntitiesDefinition::EntityList(vec![
786            self.build_statement_entity(statement),
787            self.build_sql_server_entity(server),
788        ]);
789
790        let response = self
791            .client
792            .is_authorized()
793            .policy_store_id(&self.policy_store_id)
794            .principal(
795                EntityIdentifier::builder()
796                    .entity_type("CodeMode::Statement")
797                    .entity_id(&statement.id)
798                    .build()
799                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
800            )
801            .action(
802                ActionIdentifier::builder()
803                    .action_type("CodeMode::Action")
804                    .action_id(statement.action())
805                    .build()
806                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
807            )
808            .resource(
809                EntityIdentifier::builder()
810                    .entity_type("CodeMode::Server")
811                    .entity_id(&server.server_id)
812                    .build()
813                    .map_err(|e| AvpError::SdkError(e.to_string()))?,
814            )
815            .entities(entities)
816            .send()
817            .await
818            .map_err(|e| AvpError::SdkError(e.to_string()))?;
819
820        Ok(self.parse_response(&response))
821    }
822
823    fn build_statement_entity(&self, statement: &StatementEntity) -> EntityItem {
824        let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
825        attrs.insert(
826            "statementType".into(),
827            AttributeValue::String(statement.statement_type.clone()),
828        );
829        attrs.insert("tables".into(), Self::string_set(&statement.tables));
830        attrs.insert("columns".into(), Self::string_set(&statement.columns));
831        attrs.insert(
832            "hasWhere".into(),
833            AttributeValue::Boolean(statement.has_where),
834        );
835        attrs.insert(
836            "hasLimit".into(),
837            AttributeValue::Boolean(statement.has_limit),
838        );
839        attrs.insert(
840            "hasOrderBy".into(),
841            AttributeValue::Boolean(statement.has_order_by),
842        );
843        attrs.insert(
844            "estimatedRows".into(),
845            AttributeValue::Long(statement.estimated_rows as i64),
846        );
847        attrs.insert(
848            "joinCount".into(),
849            AttributeValue::Long(statement.join_count as i64),
850        );
851        attrs.insert(
852            "subqueryCount".into(),
853            AttributeValue::Long(statement.subquery_count as i64),
854        );
855
856        EntityItem::builder()
857            .identifier(
858                EntityIdentifier::builder()
859                    .entity_type("CodeMode::Statement")
860                    .entity_id(&statement.id)
861                    .build()
862                    .expect("valid entity identifier"),
863            )
864            .set_attributes(Some(attrs))
865            .build()
866    }
867
868    fn build_sql_server_entity(&self, server: &SqlServerEntity) -> EntityItem {
869        let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
870        attrs.insert(
871            "serverId".into(),
872            AttributeValue::String(server.server_id.clone()),
873        );
874        attrs.insert(
875            "serverType".into(),
876            AttributeValue::String(server.server_type.clone()),
877        );
878        attrs.insert(
879            "allowWrite".into(),
880            AttributeValue::Boolean(server.allow_write),
881        );
882        attrs.insert(
883            "allowDelete".into(),
884            AttributeValue::Boolean(server.allow_delete),
885        );
886        attrs.insert(
887            "allowAdmin".into(),
888            AttributeValue::Boolean(server.allow_admin),
889        );
890        attrs.insert(
891            "maxRows".into(),
892            AttributeValue::Long(server.max_rows as i64),
893        );
894        attrs.insert(
895            "maxJoins".into(),
896            AttributeValue::Long(server.max_joins as i64),
897        );
898        attrs.insert(
899            "allowedOperations".into(),
900            Self::string_set(&server.allowed_operations),
901        );
902        attrs.insert(
903            "blockedOperations".into(),
904            Self::string_set(&server.blocked_operations),
905        );
906        attrs.insert(
907            "blockedTables".into(),
908            Self::string_set(&server.blocked_tables),
909        );
910        attrs.insert(
911            "blockedColumns".into(),
912            Self::string_set(&server.blocked_columns),
913        );
914
915        EntityItem::builder()
916            .identifier(
917                EntityIdentifier::builder()
918                    .entity_type("CodeMode::Server")
919                    .entity_id(&server.server_id)
920                    .build()
921                    .expect("valid entity identifier"),
922            )
923            .set_attributes(Some(attrs))
924            .build()
925    }
926}
927
928/// AVP-based policy evaluator implementing the [`PolicyEvaluator`] trait.
929///
930/// This wraps [`AvpClient`] and provides the standard `PolicyEvaluator` interface
931/// for use with `ValidationPipeline` and `#[derive(CodeMode)]`.
932///
933/// # Example
934///
935/// ```rust,ignore
936/// use pmcp_code_mode::{AvpClient, AvpConfig, AvpPolicyEvaluator, NoopPolicyEvaluator};
937/// use std::sync::Arc;
938///
939/// // Runtime selection based on environment
940/// let evaluator: Arc<dyn PolicyEvaluator> = match std::env::var("POLICY_STORE_ID") {
941///     Ok(store_id) => Arc::new(AvpPolicyEvaluator::new(
942///         AvpClient::new(AvpConfig { policy_store_id: store_id, region: None }).await?
943///     )),
944///     Err(_) => Arc::new(NoopPolicyEvaluator::new()),
945/// };
946/// ```
947pub struct AvpPolicyEvaluator {
948    client: AvpClient,
949}
950
951impl AvpPolicyEvaluator {
952    /// Create a new AVP policy evaluator.
953    pub fn new(client: AvpClient) -> Self {
954        Self { client }
955    }
956}
957
958#[async_trait::async_trait]
959impl PolicyEvaluator for AvpPolicyEvaluator {
960    async fn evaluate_operation(
961        &self,
962        operation: &OperationEntity,
963        server_config: &ServerConfigEntity,
964    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
965        self.client
966            .is_authorized(operation, server_config)
967            .await
968            .map_err(|e| PolicyEvaluationError::EvaluationError(e.to_string()))
969    }
970
971    #[cfg(feature = "openapi-code-mode")]
972    async fn evaluate_script(
973        &self,
974        script: &ScriptEntity,
975        server: &OpenAPIServerEntity,
976    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
977        self.client
978            .is_script_authorized(script, server)
979            .await
980            .map_err(|e| PolicyEvaluationError::EvaluationError(e.to_string()))
981    }
982
983    #[cfg(feature = "sql-code-mode")]
984    async fn evaluate_statement(
985        &self,
986        statement: &StatementEntity,
987        server: &SqlServerEntity,
988    ) -> Result<AuthorizationDecision, PolicyEvaluationError> {
989        self.client
990            .is_statement_authorized(statement, server)
991            .await
992            .map_err(|e| PolicyEvaluationError::EvaluationError(e.to_string()))
993    }
994
995    fn name(&self) -> &str {
996        "avp"
997    }
998}