1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct AvpConfig {
48 pub policy_store_id: String,
50
51 #[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#[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#[derive(Clone)]
84pub struct AvpClient {
85 client: Client,
86 policy_store_id: String,
87}
88
89impl AvpClient {
90 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 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 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 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#[cfg(feature = "openapi-code-mode")]
499impl AvpClient {
500 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
770pub struct AvpPolicyEvaluator {
790 client: AvpClient,
791}
792
793impl AvpPolicyEvaluator {
794 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}