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