1use async_trait::async_trait;
15use chrono::{DateTime, Utc};
16use http::{Method, StatusCode};
17use serde_json::{json, Map, Value};
18use std::sync::Arc;
19use tokio::sync::Mutex as AsyncMutex;
20
21use fakecloud_core::pagination::paginate_checked;
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::state::{
26 invitation_arn, managed_permission_arn, permission_arn, resource_share_arn, AssociationRecord,
27 InvitationRecord, PermissionRecord, PermissionVersionRecord, ResourceShareRecord,
28 SharedRamState, TagMap,
29};
30use crate::validate;
31
32pub const RAM_ACTIONS: &[&str] = &[
33 "AcceptResourceShareInvitation",
34 "AssociateResourceShare",
35 "AssociateResourceSharePermission",
36 "CreatePermission",
37 "CreatePermissionVersion",
38 "CreateResourceShare",
39 "DeletePermission",
40 "DeletePermissionVersion",
41 "DeleteResourceShare",
42 "DisassociateResourceShare",
43 "DisassociateResourceSharePermission",
44 "EnableSharingWithAwsOrganization",
45 "GetPermission",
46 "GetResourcePolicies",
47 "GetResourceShareAssociations",
48 "GetResourceShareInvitations",
49 "GetResourceShares",
50 "ListPendingInvitationResources",
51 "ListPermissionAssociations",
52 "ListPermissionVersions",
53 "ListPermissions",
54 "ListPrincipals",
55 "ListReplacePermissionAssociationsWork",
56 "ListResourceSharePermissions",
57 "ListResourceTypes",
58 "ListResources",
59 "ListSourceAssociations",
60 "PromotePermissionCreatedFromPolicy",
61 "PromoteResourceShareCreatedFromPolicy",
62 "RejectResourceShareInvitation",
63 "ReplacePermissionAssociations",
64 "SetDefaultPermissionVersion",
65 "TagResource",
66 "UntagResource",
67 "UpdateResourceShare",
68];
69
70const MUTATING: &[&str] = &[
71 "AcceptResourceShareInvitation",
72 "AssociateResourceShare",
73 "AssociateResourceSharePermission",
74 "CreatePermission",
75 "CreatePermissionVersion",
76 "CreateResourceShare",
77 "DeletePermission",
78 "DeletePermissionVersion",
79 "DeleteResourceShare",
80 "DisassociateResourceShare",
81 "DisassociateResourceSharePermission",
82 "PromotePermissionCreatedFromPolicy",
86 "PromoteResourceShareCreatedFromPolicy",
87 "RejectResourceShareInvitation",
88 "ReplacePermissionAssociations",
89 "SetDefaultPermissionVersion",
90 "TagResource",
91 "UntagResource",
92 "UpdateResourceShare",
93];
94
95const MANAGED_PERMISSIONS: &[(&str, &str)] = &[
97 ("AWSRAMDefaultPermissionSubnet", "ec2:Subnet"),
98 (
99 "AWSRAMDefaultPermissionTransitGateway",
100 "ec2:TransitGateway",
101 ),
102 (
103 "AWSRAMDefaultPermissionResolverRule",
104 "route53resolver:ResolverRule",
105 ),
106 ("AWSRAMDefaultPermissionPrefixList", "ec2:PrefixList"),
107 (
108 "AWSRAMDefaultPermissionLicenseConfiguration",
109 "license-manager:LicenseConfiguration",
110 ),
111];
112
113const RESOURCE_TYPES: &[(&str, &str, &str)] = &[
115 ("ec2:Subnet", "ec2", "REGIONAL"),
116 ("ec2:TransitGateway", "ec2", "REGIONAL"),
117 ("ec2:PrefixList", "ec2", "REGIONAL"),
118 ("ec2:LocalGatewayRouteTable", "ec2", "REGIONAL"),
119 (
120 "route53resolver:ResolverRule",
121 "route53resolver",
122 "REGIONAL",
123 ),
124 (
125 "license-manager:LicenseConfiguration",
126 "license-manager",
127 "GLOBAL",
128 ),
129];
130
131pub struct RamService {
132 state: SharedRamState,
133 snapshot_store: Option<Arc<dyn SnapshotStore>>,
134 snapshot_lock: Arc<AsyncMutex<()>>,
135}
136
137impl RamService {
138 pub fn new(state: SharedRamState) -> Self {
139 Self {
140 state,
141 snapshot_store: None,
142 snapshot_lock: Arc::new(AsyncMutex::new(())),
143 }
144 }
145
146 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
147 self.snapshot_store = Some(store);
148 self
149 }
150
151 async fn save_snapshot(&self) {
152 crate::persistence::save_snapshot(
153 &self.state,
154 self.snapshot_store.clone(),
155 &self.snapshot_lock,
156 )
157 .await;
158 }
159
160 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
162 let store = self.snapshot_store.clone()?;
163 let state = self.state.clone();
164 let lock = self.snapshot_lock.clone();
165 Some(Arc::new(move || {
166 let state = state.clone();
167 let store = store.clone();
168 let lock = lock.clone();
169 Box::pin(async move {
170 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
171 })
172 }))
173 }
174
175 fn resolve_action(req: &AwsRequest) -> Option<&'static str> {
177 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
178 let seg = raw.strip_prefix('/').unwrap_or(raw);
179 let seg = seg.strip_suffix('/').unwrap_or(seg).to_ascii_lowercase();
180 let action = match (&req.method, seg.as_str()) {
181 (&Method::POST, "acceptresourceshareinvitation") => "AcceptResourceShareInvitation",
182 (&Method::POST, "associateresourceshare") => "AssociateResourceShare",
183 (&Method::POST, "associateresourcesharepermission") => {
184 "AssociateResourceSharePermission"
185 }
186 (&Method::POST, "createpermission") => "CreatePermission",
187 (&Method::POST, "createpermissionversion") => "CreatePermissionVersion",
188 (&Method::POST, "createresourceshare") => "CreateResourceShare",
189 (&Method::DELETE, "deletepermission") => "DeletePermission",
190 (&Method::DELETE, "deletepermissionversion") => "DeletePermissionVersion",
191 (&Method::DELETE, "deleteresourceshare") => "DeleteResourceShare",
192 (&Method::POST, "disassociateresourceshare") => "DisassociateResourceShare",
193 (&Method::POST, "disassociateresourcesharepermission") => {
194 "DisassociateResourceSharePermission"
195 }
196 (&Method::POST, "enablesharingwithawsorganization") => {
197 "EnableSharingWithAwsOrganization"
198 }
199 (&Method::POST, "getpermission") => "GetPermission",
200 (&Method::POST, "getresourcepolicies") => "GetResourcePolicies",
201 (&Method::POST, "getresourceshareassociations") => "GetResourceShareAssociations",
202 (&Method::POST, "getresourceshareinvitations") => "GetResourceShareInvitations",
203 (&Method::POST, "getresourceshares") => "GetResourceShares",
204 (&Method::POST, "listpendinginvitationresources") => "ListPendingInvitationResources",
205 (&Method::POST, "listpermissionassociations") => "ListPermissionAssociations",
206 (&Method::POST, "listpermissionversions") => "ListPermissionVersions",
207 (&Method::POST, "listpermissions") => "ListPermissions",
208 (&Method::POST, "listprincipals") => "ListPrincipals",
209 (&Method::POST, "listreplacepermissionassociationswork") => {
210 "ListReplacePermissionAssociationsWork"
211 }
212 (&Method::POST, "listresourcesharepermissions") => "ListResourceSharePermissions",
213 (&Method::POST, "listresourcetypes") => "ListResourceTypes",
214 (&Method::POST, "listresources") => "ListResources",
215 (&Method::POST, "listsourceassociations") => "ListSourceAssociations",
216 (&Method::POST, "promotepermissioncreatedfrompolicy") => {
217 "PromotePermissionCreatedFromPolicy"
218 }
219 (&Method::POST, "promoteresourcesharecreatedfrompolicy") => {
220 "PromoteResourceShareCreatedFromPolicy"
221 }
222 (&Method::POST, "rejectresourceshareinvitation") => "RejectResourceShareInvitation",
223 (&Method::POST, "replacepermissionassociations") => "ReplacePermissionAssociations",
224 (&Method::POST, "setdefaultpermissionversion") => "SetDefaultPermissionVersion",
225 (&Method::POST, "tagresource") => "TagResource",
226 (&Method::POST, "untagresource") => "UntagResource",
227 (&Method::POST, "updateresourceshare") => "UpdateResourceShare",
228 _ => return None,
229 };
230 Some(action)
231 }
232}
233
234#[async_trait]
235impl AwsService for RamService {
236 fn service_name(&self) -> &str {
237 "ram"
238 }
239
240 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
241 let action = Self::resolve_action(&req).ok_or_else(|| {
242 AwsServiceError::aws_error(
243 StatusCode::NOT_FOUND,
244 "UnknownOperationException",
245 format!("Unknown operation: {} {}", req.method, req.raw_path),
246 )
247 })?;
248
249 let result = self.dispatch(action, &req);
250
251 if MUTATING.contains(&action)
252 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
253 {
254 self.save_snapshot().await;
255 }
256 result
257 }
258
259 fn supported_actions(&self) -> &[&str] {
260 RAM_ACTIONS
261 }
262}
263
264fn ok(v: Value) -> AwsResponse {
269 AwsResponse::json_value(StatusCode::OK, v)
270}
271
272fn ts(dt: DateTime<Utc>) -> Value {
273 json!(dt.timestamp_millis() as f64 / 1000.0)
274}
275
276fn gen_id() -> String {
277 uuid::Uuid::new_v4().to_string()
278}
279
280fn aws_err(code: &str, msg: impl Into<String>) -> AwsServiceError {
281 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
282}
283fn unknown_resource(msg: impl Into<String>) -> AwsServiceError {
284 aws_err("UnknownResourceException", msg)
285}
286fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
287 aws_err("InvalidParameterException", msg)
288}
289fn invitation_not_found(msg: impl Into<String>) -> AwsServiceError {
290 aws_err("ResourceShareInvitationArnNotFoundException", msg)
291}
292
293fn req_body(req: &AwsRequest) -> Value {
294 serde_json::from_slice(&req.body).unwrap_or(Value::Null)
295}
296
297fn str_field(body: &Value, key: &str) -> Option<String> {
298 body.get(key).and_then(|v| v.as_str()).map(str::to_string)
299}
300
301fn query_str(req: &AwsRequest, key: &str) -> Option<String> {
304 req.query_params.get(key).cloned()
305}
306
307fn req_str(body: &Value, key: &str) -> Result<String, AwsServiceError> {
308 str_field(body, key).ok_or_else(|| invalid_param(format!("{key} is required.")))
309}
310
311fn bool_field(body: &Value, key: &str) -> Option<bool> {
312 body.get(key).and_then(|v| v.as_bool())
313}
314
315fn u32_field(body: &Value, key: &str) -> Option<u32> {
316 body.get(key).and_then(|v| v.as_u64()).map(|n| n as u32)
317}
318
319fn string_list(body: &Value, key: &str) -> Vec<String> {
320 body.get(key)
321 .and_then(|v| v.as_array())
322 .map(|a| {
323 a.iter()
324 .filter_map(|x| x.as_str().map(str::to_string))
325 .collect()
326 })
327 .unwrap_or_default()
328}
329
330fn parse_tag_list(body: &Value, key: &str) -> TagMap {
332 let mut out = TagMap::new();
333 if let Some(arr) = body.get(key).and_then(|v| v.as_array()) {
334 for t in arr {
335 if let (Some(k), Some(v)) = (
336 t.get("key").and_then(|x| x.as_str()),
337 t.get("value").and_then(|x| x.as_str()),
338 ) {
339 out.insert(k.to_string(), v.to_string());
340 }
341 }
342 }
343 out
344}
345
346fn tag_list_value(t: &TagMap) -> Value {
347 Value::Array(
348 t.iter()
349 .map(|(k, v)| json!({ "key": k, "value": v }))
350 .collect(),
351 )
352}
353
354fn owner_matches(share: &ResourceShareRecord, owner: &str, account_id: &str) -> bool {
359 if owner == "SELF" {
360 share.owning_account_id == account_id
361 } else {
362 share.owning_account_id != account_id
363 }
364}
365
366fn ram_resource_type(arn: &str) -> String {
370 let parts: Vec<&str> = arn.split(':').collect();
371 if parts.len() >= 6 {
372 let service = parts[2];
373 let resource = parts[5..].join(":");
374 let kind = resource.split(['/', ':']).next().unwrap_or(&resource);
375 if !service.is_empty() && !kind.is_empty() {
376 let pascal: String = kind
377 .split('-')
378 .map(|w| {
379 let mut c = w.chars();
380 match c.next() {
381 Some(f) => f.to_ascii_uppercase().to_string() + c.as_str(),
382 None => String::new(),
383 }
384 })
385 .collect();
386 return format!("{service}:{pascal}");
387 }
388 }
389 "unknown".to_string()
390}
391
392fn is_external_principal(principal: &str, account_id: &str) -> bool {
395 if principal == account_id {
396 return false;
397 }
398 let is_account = principal.len() == 12 && principal.chars().all(|c| c.is_ascii_digit());
399 is_account || principal.starts_with("arn:aws:organizations::")
400}
401
402fn page_response(
404 items: Vec<Value>,
405 list_key: &str,
406 body: &Value,
407 extra: &[(&str, Value)],
408) -> Result<AwsResponse, AwsServiceError> {
409 let max = body
410 .get("maxResults")
411 .and_then(|v| v.as_u64())
412 .map(|n| n as usize)
413 .filter(|n| *n > 0)
414 .unwrap_or(500);
415 let token = body
416 .get("nextToken")
417 .and_then(|v| v.as_str())
418 .map(str::to_string);
419 let (page, next) = paginate_checked(&items, token.as_deref(), max)
420 .map_err(|_| aws_err("InvalidNextTokenException", "Invalid nextToken"))?;
421 let mut out = Map::new();
422 out.insert(list_key.to_string(), Value::Array(page));
423 if let Some(t) = next {
424 out.insert("nextToken".to_string(), Value::String(t));
425 }
426 for (k, v) in extra {
427 out.insert((*k).to_string(), v.clone());
428 }
429 Ok(ok(Value::Object(out)))
430}
431
432fn share_to_value(r: &ResourceShareRecord) -> Value {
437 let mut m = Map::new();
438 m.insert("resourceShareArn".into(), json!(r.arn));
439 m.insert("name".into(), json!(r.name));
440 m.insert("owningAccountId".into(), json!(r.owning_account_id));
441 m.insert(
442 "allowExternalPrincipals".into(),
443 json!(r.allow_external_principals),
444 );
445 m.insert("status".into(), json!(r.status));
446 if let Some(msg) = &r.status_message {
447 m.insert("statusMessage".into(), json!(msg));
448 }
449 m.insert("tags".into(), tag_list_value(&r.tags));
450 m.insert("creationTime".into(), ts(r.creation_time));
451 m.insert("lastUpdatedTime".into(), ts(r.last_updated_time));
452 m.insert("featureSet".into(), json!(r.feature_set));
453 if let Some(retain) = r.retain_sharing {
454 m.insert(
455 "resourceShareConfiguration".into(),
456 json!({ "retainSharingOnAccountLeaveOrganization": retain }),
457 );
458 }
459 Value::Object(m)
460}
461
462fn association_to_value(arn: &str, name: &str, a: &AssociationRecord) -> Value {
463 let mut m = Map::new();
464 m.insert("resourceShareArn".into(), json!(arn));
465 m.insert("resourceShareName".into(), json!(name));
466 m.insert("associatedEntity".into(), json!(a.associated_entity));
467 m.insert("associationType".into(), json!(a.association_type));
468 m.insert("status".into(), json!(a.status));
469 if let Some(msg) = &a.status_message {
470 m.insert("statusMessage".into(), json!(msg));
471 }
472 m.insert("creationTime".into(), ts(a.creation_time));
473 m.insert("lastUpdatedTime".into(), ts(a.last_updated_time));
474 m.insert("external".into(), json!(a.external));
475 Value::Object(m)
476}
477
478fn invitation_to_value(inv: &InvitationRecord) -> Value {
479 json!({
480 "resourceShareInvitationArn": inv.arn,
481 "resourceShareName": inv.resource_share_name,
482 "resourceShareArn": inv.resource_share_arn,
483 "senderAccountId": inv.sender_account_id,
484 "receiverAccountId": inv.receiver_account_id,
485 "invitationTimestamp": ts(inv.invitation_timestamp),
486 "status": inv.status,
487 })
488}
489
490fn managed_summary(name: &str, resource_type: &str, now: DateTime<Utc>) -> Value {
491 json!({
492 "arn": managed_permission_arn(name),
493 "version": "1",
494 "defaultVersion": true,
495 "name": name,
496 "resourceType": resource_type,
497 "status": "ATTACHABLE",
498 "creationTime": ts(now),
499 "lastUpdatedTime": ts(now),
500 "isResourceTypeDefault": true,
501 "permissionType": "AWS_MANAGED",
502 "featureSet": "STANDARD",
503 "tags": [],
504 })
505}
506
507fn customer_summary(p: &PermissionRecord) -> Value {
508 json!({
509 "arn": p.arn,
510 "version": p.default_version.to_string(),
511 "defaultVersion": true,
512 "name": p.name,
513 "resourceType": p.resource_type,
514 "status": p.status,
515 "creationTime": ts(p.creation_time),
516 "lastUpdatedTime": ts(p.last_updated_time),
517 "isResourceTypeDefault": false,
518 "permissionType": "CUSTOMER_MANAGED",
519 "featureSet": p.feature_set,
520 "tags": tag_list_value(&p.tags),
521 })
522}
523
524fn customer_detail(p: &PermissionRecord, v: &PermissionVersionRecord) -> Value {
525 json!({
526 "arn": p.arn,
527 "version": v.version.to_string(),
528 "defaultVersion": v.is_default,
529 "name": p.name,
530 "resourceType": p.resource_type,
531 "permission": v.policy_template,
532 "creationTime": ts(v.creation_time),
533 "lastUpdatedTime": ts(v.last_updated_time),
534 "isResourceTypeDefault": false,
535 "permissionType": "CUSTOMER_MANAGED",
536 "featureSet": p.feature_set,
537 "status": p.status,
538 "tags": tag_list_value(&p.tags),
539 })
540}
541
542fn managed_detail(name: &str, resource_type: &str, now: DateTime<Utc>) -> Value {
543 let policy = json!({
544 "Effect": "Allow",
545 "Action": [format!("{}:*", resource_type.split(':').next().unwrap_or("*"))],
546 })
547 .to_string();
548 json!({
549 "arn": managed_permission_arn(name),
550 "version": "1",
551 "defaultVersion": true,
552 "name": name,
553 "resourceType": resource_type,
554 "permission": policy,
555 "creationTime": ts(now),
556 "lastUpdatedTime": ts(now),
557 "isResourceTypeDefault": true,
558 "permissionType": "AWS_MANAGED",
559 "featureSet": "STANDARD",
560 "status": "ATTACHABLE",
561 "tags": [],
562 })
563}
564
565fn find_managed(arn: &str) -> Option<(&'static str, &'static str)> {
566 MANAGED_PERMISSIONS
567 .iter()
568 .find(|(name, _)| managed_permission_arn(name) == arn)
569 .copied()
570}
571
572impl RamService {
577 #[allow(clippy::too_many_lines)]
578 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
579 validate::validate(action, req)?;
580 let body = req_body(req);
581 match action {
582 "CreateResourceShare" => self.create_resource_share(req, &body),
583 "UpdateResourceShare" => self.update_resource_share(req, &body),
584 "DeleteResourceShare" => self.delete_resource_share(req, &body),
585 "GetResourceShares" => self.get_resource_shares(req, &body),
586 "AssociateResourceShare" => self.associate_resource_share(req, &body, true),
587 "DisassociateResourceShare" => self.associate_resource_share(req, &body, false),
588 "AssociateResourceSharePermission" => self.associate_permission(req, &body, true),
589 "DisassociateResourceSharePermission" => self.associate_permission(req, &body, false),
590 "GetResourceShareAssociations" => self.get_share_associations(req, &body),
591 "GetResourceShareInvitations" => self.get_invitations(req, &body),
592 "AcceptResourceShareInvitation" => self.respond_invitation(req, &body, true),
593 "RejectResourceShareInvitation" => self.respond_invitation(req, &body, false),
594 "ListPendingInvitationResources" => self.list_pending_invitation_resources(req, &body),
595 "EnableSharingWithAwsOrganization" => Ok(ok(json!({ "returnValue": true }))),
596 "CreatePermission" => self.create_permission(req, &body),
597 "CreatePermissionVersion" => self.create_permission_version(req, &body),
598 "DeletePermission" => self.delete_permission(req, &body),
599 "DeletePermissionVersion" => self.delete_permission_version(req, &body),
600 "GetPermission" => self.get_permission(req, &body),
601 "ListPermissions" => self.list_permissions(req, &body),
602 "ListPermissionVersions" => self.list_permission_versions(req, &body),
603 "ListResourceSharePermissions" => self.list_resource_share_permissions(req, &body),
604 "SetDefaultPermissionVersion" => self.set_default_permission_version(req, &body),
605 "PromotePermissionCreatedFromPolicy" => self.promote_permission(req, &body),
606 "PromoteResourceShareCreatedFromPolicy" => self.promote_resource_share(req, &body),
607 "ReplacePermissionAssociations" => self.replace_permission_associations(req, &body),
608 "ListReplacePermissionAssociationsWork" => self.list_replace_work(req, &body),
609 "ListPermissionAssociations" => self.list_permission_associations(req, &body),
610 "ListPrincipals" => self.list_principals(req, &body),
611 "ListResources" => self.list_resources(req, &body),
612 "ListSourceAssociations" => page_response(Vec::new(), "sourceAssociations", &body, &[]),
613 "ListResourceTypes" => self.list_resource_types(req, &body),
614 "GetResourcePolicies" => self.get_resource_policies(req, &body),
615 "TagResource" => self.tag_resource(req, &body),
616 "UntagResource" => self.untag_resource(req, &body),
617 _ => Err(AwsServiceError::action_not_implemented("ram", action)),
618 }
619 }
620
621 fn create_resource_share(
624 &self,
625 req: &AwsRequest,
626 body: &Value,
627 ) -> Result<AwsResponse, AwsServiceError> {
628 let name = req_str(body, "name")?;
629 let now = Utc::now();
630 let id = gen_id();
631 let arn = resource_share_arn(&req.region, &req.account_id, &id);
632 let tags = parse_tag_list(body, "tags");
633 let retain = body
634 .get("resourceShareConfiguration")
635 .and_then(|c| c.get("retainSharingOnAccountLeaveOrganization"))
636 .and_then(|v| v.as_bool());
637
638 let mut resource_associations = Vec::new();
639 for r in string_list(body, "resourceArns") {
640 resource_associations.push(AssociationRecord {
641 associated_entity: r,
642 association_type: "RESOURCE".into(),
643 status: "ASSOCIATED".into(),
644 status_message: None,
645 creation_time: now,
646 last_updated_time: now,
647 external: false,
648 });
649 }
650 let mut principal_associations = Vec::new();
651 let mut invitations = Vec::new();
652 for p in string_list(body, "principals") {
653 let external = is_external_principal(&p, &req.account_id);
654 principal_associations.push(AssociationRecord {
655 associated_entity: p.clone(),
656 association_type: "PRINCIPAL".into(),
657 status: "ASSOCIATED".into(),
658 status_message: None,
659 creation_time: now,
660 last_updated_time: now,
661 external,
662 });
663 if external && p.len() == 12 && p.chars().all(|c| c.is_ascii_digit()) {
665 invitations.push(InvitationRecord {
666 arn: invitation_arn(&req.region, &req.account_id, &gen_id()),
667 resource_share_arn: arn.clone(),
668 resource_share_name: name.clone(),
669 sender_account_id: req.account_id.clone(),
670 receiver_account_id: p.clone(),
671 invitation_timestamp: now,
672 status: "PENDING".into(),
673 });
674 }
675 }
676
677 let record = ResourceShareRecord {
678 arn: arn.clone(),
679 name,
680 owning_account_id: req.account_id.clone(),
681 allow_external_principals: bool_field(body, "allowExternalPrincipals").unwrap_or(true),
682 status: "ACTIVE".into(),
683 status_message: None,
684 creation_time: now,
685 last_updated_time: now,
686 feature_set: "STANDARD".into(),
687 tags,
688 retain_sharing: retain,
689 resource_associations,
690 principal_associations,
691 permission_arns: string_list(body, "permissionArns"),
692 };
693 let value = share_to_value(&record);
694
695 let mut accounts = self.state.write();
696 accounts
697 .get_or_create(&req.account_id)
698 .resource_shares
699 .insert(arn, record);
700 for inv in invitations {
704 let receiver = inv.receiver_account_id.clone();
705 accounts
706 .get_or_create(&receiver)
707 .invitations
708 .insert(inv.arn.clone(), inv);
709 }
710
711 Ok(ok(json!({
712 "resourceShare": value,
713 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
714 })))
715 }
716
717 fn update_resource_share(
718 &self,
719 req: &AwsRequest,
720 body: &Value,
721 ) -> Result<AwsResponse, AwsServiceError> {
722 let arn = req_str(body, "resourceShareArn")?;
723 let mut accounts = self.state.write();
724 let st = accounts.get_or_create(&req.account_id);
725 let share = st
726 .resource_shares
727 .get_mut(&arn)
728 .ok_or_else(|| unknown_resource(format!("ResourceShare {arn} could not be found.")))?;
729 if let Some(name) = str_field(body, "name") {
730 share.name = name;
731 }
732 if let Some(allow) = bool_field(body, "allowExternalPrincipals") {
733 share.allow_external_principals = allow;
734 }
735 share.last_updated_time = Utc::now();
736 let value = share_to_value(share);
737 Ok(ok(json!({
738 "resourceShare": value,
739 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
740 })))
741 }
742
743 fn delete_resource_share(
744 &self,
745 req: &AwsRequest,
746 _body: &Value,
747 ) -> Result<AwsResponse, AwsServiceError> {
748 let arn = query_str(req, "resourceShareArn")
749 .ok_or_else(|| invalid_param("resourceShareArn is required."))?;
750 let mut accounts = self.state.write();
751 let st = accounts.get_or_create(&req.account_id);
752 if st.resource_shares.remove(&arn).is_none() {
753 return Err(unknown_resource(format!(
754 "ResourceShare {arn} could not be found."
755 )));
756 }
757 st.tags.remove(&arn);
758 Ok(ok(json!({
759 "returnValue": true,
760 "clientToken": query_str(req, "clientToken").unwrap_or_else(gen_id),
761 })))
762 }
763
764 fn get_resource_shares(
765 &self,
766 req: &AwsRequest,
767 body: &Value,
768 ) -> Result<AwsResponse, AwsServiceError> {
769 let owner = req_str(body, "resourceOwner")?;
770 let arns_filter = string_list(body, "resourceShareArns");
771 let status_filter = str_field(body, "resourceShareStatus");
772 let name_filter = str_field(body, "name");
773 let accounts = self.state.read();
774 let items: Vec<Value> = accounts
777 .get(&req.account_id)
778 .map(|st| {
779 st.resource_shares
780 .values()
781 .filter(|s| owner_matches(s, &owner, &req.account_id))
782 .filter(|s| arns_filter.is_empty() || arns_filter.contains(&s.arn))
783 .filter(|s| status_filter.as_deref().is_none_or(|st| st == s.status))
784 .filter(|s| name_filter.as_deref().is_none_or(|n| n == s.name))
785 .map(share_to_value)
786 .collect()
787 })
788 .unwrap_or_default();
789 page_response(items, "resourceShares", body, &[])
790 }
791
792 fn associate_resource_share(
793 &self,
794 req: &AwsRequest,
795 body: &Value,
796 associate: bool,
797 ) -> Result<AwsResponse, AwsServiceError> {
798 let arn = req_str(body, "resourceShareArn")?;
799 let now = Utc::now();
800 let mut accounts = self.state.write();
801 let st = accounts.get_or_create(&req.account_id);
802 let share = st
803 .resource_shares
804 .get_mut(&arn)
805 .ok_or_else(|| unknown_resource(format!("ResourceShare {arn} could not be found.")))?;
806 let name = share.name.clone();
807 let mut affected: Vec<Value> = Vec::new();
808 let status = if associate {
809 "ASSOCIATED"
810 } else {
811 "DISASSOCIATED"
812 };
813
814 for r in string_list(body, "resourceArns") {
815 if associate {
816 share.resource_associations.push(AssociationRecord {
817 associated_entity: r.clone(),
818 association_type: "RESOURCE".into(),
819 status: status.into(),
820 status_message: None,
821 creation_time: now,
822 last_updated_time: now,
823 external: false,
824 });
825 } else {
826 share
827 .resource_associations
828 .retain(|a| a.associated_entity != r);
829 }
830 affected.push(association_to_value(
831 &arn,
832 &name,
833 &AssociationRecord {
834 associated_entity: r,
835 association_type: "RESOURCE".into(),
836 status: status.into(),
837 status_message: None,
838 creation_time: now,
839 last_updated_time: now,
840 external: false,
841 },
842 ));
843 }
844 for p in string_list(body, "principals") {
845 let external = is_external_principal(&p, &req.account_id);
846 if associate {
847 share.principal_associations.push(AssociationRecord {
848 associated_entity: p.clone(),
849 association_type: "PRINCIPAL".into(),
850 status: status.into(),
851 status_message: None,
852 creation_time: now,
853 last_updated_time: now,
854 external,
855 });
856 } else {
857 share
858 .principal_associations
859 .retain(|a| a.associated_entity != p);
860 }
861 affected.push(association_to_value(
862 &arn,
863 &name,
864 &AssociationRecord {
865 associated_entity: p,
866 association_type: "PRINCIPAL".into(),
867 status: status.into(),
868 status_message: None,
869 creation_time: now,
870 last_updated_time: now,
871 external,
872 },
873 ));
874 }
875 share.last_updated_time = now;
876 Ok(ok(json!({
877 "resourceShareAssociations": affected,
878 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
879 })))
880 }
881
882 fn associate_permission(
883 &self,
884 req: &AwsRequest,
885 body: &Value,
886 associate: bool,
887 ) -> Result<AwsResponse, AwsServiceError> {
888 let arn = req_str(body, "resourceShareArn")?;
889 let permission = req_str(body, "permissionArn")?;
890 let mut accounts = self.state.write();
891 let st = accounts.get_or_create(&req.account_id);
892 let share = st
893 .resource_shares
894 .get_mut(&arn)
895 .ok_or_else(|| unknown_resource(format!("ResourceShare {arn} could not be found.")))?;
896 if associate {
897 if !share.permission_arns.contains(&permission) {
898 share.permission_arns.push(permission);
899 }
900 } else {
901 share.permission_arns.retain(|p| p != &permission);
902 }
903 share.last_updated_time = Utc::now();
904 Ok(ok(json!({
905 "returnValue": true,
906 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
907 })))
908 }
909
910 fn get_share_associations(
911 &self,
912 _req: &AwsRequest,
913 body: &Value,
914 ) -> Result<AwsResponse, AwsServiceError> {
915 let assoc_type = req_str(body, "associationType")?;
916 let arns_filter = string_list(body, "resourceShareArns");
917 let status_filter = str_field(body, "associationStatus");
918 let accounts = self.state.read();
919 let mut items: Vec<Value> = Vec::new();
920 if let Some(st) = accounts.get(&_req.account_id) {
921 for share in st.resource_shares.values() {
922 if !arns_filter.is_empty() && !arns_filter.contains(&share.arn) {
923 continue;
924 }
925 let src = if assoc_type == "PRINCIPAL" {
926 &share.principal_associations
927 } else {
928 &share.resource_associations
929 };
930 for a in src {
931 if status_filter.as_deref().is_some_and(|s| s != a.status) {
932 continue;
933 }
934 items.push(association_to_value(&share.arn, &share.name, a));
935 }
936 }
937 }
938 page_response(items, "resourceShareAssociations", body, &[])
939 }
940
941 fn get_invitations(
942 &self,
943 req: &AwsRequest,
944 body: &Value,
945 ) -> Result<AwsResponse, AwsServiceError> {
946 let inv_filter = string_list(body, "resourceShareInvitationArns");
947 let share_filter = string_list(body, "resourceShareArns");
948 let accounts = self.state.read();
949 let items: Vec<Value> = accounts
950 .get(&req.account_id)
951 .map(|st| {
952 st.invitations
953 .values()
954 .filter(|i| inv_filter.is_empty() || inv_filter.contains(&i.arn))
955 .filter(|i| {
956 share_filter.is_empty() || share_filter.contains(&i.resource_share_arn)
957 })
958 .map(invitation_to_value)
959 .collect()
960 })
961 .unwrap_or_default();
962 page_response(items, "resourceShareInvitations", body, &[])
963 }
964
965 fn respond_invitation(
966 &self,
967 req: &AwsRequest,
968 body: &Value,
969 accept: bool,
970 ) -> Result<AwsResponse, AwsServiceError> {
971 let arn = req_str(body, "resourceShareInvitationArn")?;
972 let mut accounts = self.state.write();
973 let st = accounts.get_or_create(&req.account_id);
975 let inv = st.invitations.get_mut(&arn).ok_or_else(|| {
976 invitation_not_found(format!("ResourceShareInvitation {arn} could not be found."))
977 })?;
978 if inv.status != "PENDING" {
979 let code = if inv.status == "ACCEPTED" {
980 "ResourceShareInvitationAlreadyAcceptedException"
981 } else {
982 "ResourceShareInvitationAlreadyRejectedException"
983 };
984 return Err(aws_err(code, format!("Invitation {arn} is {}", inv.status)));
985 }
986 inv.status = if accept { "ACCEPTED" } else { "REJECTED" }.into();
987 let value = invitation_to_value(inv);
988 let sender = inv.sender_account_id.clone();
989 let share_arn = inv.resource_share_arn.clone();
990
991 if accept {
992 if let Some(shared) = accounts
998 .get(&sender)
999 .and_then(|s| s.resource_shares.get(&share_arn))
1000 .cloned()
1001 {
1002 accounts
1003 .get_or_create(&req.account_id)
1004 .resource_shares
1005 .insert(share_arn, shared);
1006 }
1007 }
1008 Ok(ok(json!({
1009 "resourceShareInvitation": value,
1010 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
1011 })))
1012 }
1013
1014 fn list_pending_invitation_resources(
1015 &self,
1016 req: &AwsRequest,
1017 body: &Value,
1018 ) -> Result<AwsResponse, AwsServiceError> {
1019 let arn = req_str(body, "resourceShareInvitationArn")?;
1020 let accounts = self.state.read();
1021 let found = accounts
1022 .get(&req.account_id)
1023 .is_some_and(|st| st.invitations.contains_key(&arn));
1024 if !found {
1025 return Err(invitation_not_found(format!(
1026 "ResourceShareInvitation {arn} could not be found."
1027 )));
1028 }
1029 page_response(Vec::new(), "resources", body, &[])
1030 }
1031
1032 fn create_permission(
1035 &self,
1036 req: &AwsRequest,
1037 body: &Value,
1038 ) -> Result<AwsResponse, AwsServiceError> {
1039 let name = req_str(body, "name")?;
1040 let resource_type = req_str(body, "resourceType")?;
1041 let policy = req_str(body, "policyTemplate")?;
1042 let now = Utc::now();
1043 let arn = permission_arn(&req.region, &req.account_id, &name);
1044 let mut accounts = self.state.write();
1045 let st = accounts.get_or_create(&req.account_id);
1046 if st.permissions.contains_key(&arn) {
1047 return Err(aws_err(
1048 "PermissionAlreadyExistsException",
1049 format!("A permission named {name} already exists."),
1050 ));
1051 }
1052 let record = PermissionRecord {
1053 arn: arn.clone(),
1054 name,
1055 resource_type,
1056 status: "ATTACHABLE".into(),
1057 feature_set: "STANDARD".into(),
1058 creation_time: now,
1059 last_updated_time: now,
1060 default_version: 1,
1061 versions: vec![PermissionVersionRecord {
1062 version: 1,
1063 policy_template: policy,
1064 is_default: true,
1065 creation_time: now,
1066 last_updated_time: now,
1067 }],
1068 tags: parse_tag_list(body, "tags"),
1069 };
1070 let value = customer_summary(&record);
1071 st.permissions.insert(arn, record);
1072 Ok(ok(json!({
1073 "permission": value,
1074 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
1075 })))
1076 }
1077
1078 fn create_permission_version(
1079 &self,
1080 req: &AwsRequest,
1081 body: &Value,
1082 ) -> Result<AwsResponse, AwsServiceError> {
1083 let arn = req_str(body, "permissionArn")?;
1084 let policy = req_str(body, "policyTemplate")?;
1085 let now = Utc::now();
1086 let mut accounts = self.state.write();
1087 let st = accounts.get_or_create(&req.account_id);
1088 let p = st
1089 .permissions
1090 .get_mut(&arn)
1091 .ok_or_else(|| unknown_resource(format!("Permission {arn} could not be found.")))?;
1092 let next = p.versions.iter().map(|v| v.version).max().unwrap_or(0) + 1;
1093 let rec = PermissionVersionRecord {
1094 version: next,
1095 policy_template: policy,
1096 is_default: false,
1097 creation_time: now,
1098 last_updated_time: now,
1099 };
1100 p.versions.push(rec.clone());
1101 p.last_updated_time = now;
1102 Ok(ok(json!({
1103 "permission": customer_detail(p, &rec),
1104 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
1105 })))
1106 }
1107
1108 fn delete_permission(
1109 &self,
1110 req: &AwsRequest,
1111 _body: &Value,
1112 ) -> Result<AwsResponse, AwsServiceError> {
1113 let arn = query_str(req, "permissionArn")
1114 .ok_or_else(|| invalid_param("permissionArn is required."))?;
1115 let mut accounts = self.state.write();
1116 let st = accounts.get_or_create(&req.account_id);
1117 if st.permissions.remove(&arn).is_none() {
1118 return Err(unknown_resource(format!(
1119 "Permission {arn} could not be found."
1120 )));
1121 }
1122 Ok(ok(json!({
1123 "returnValue": true,
1124 "clientToken": query_str(req, "clientToken").unwrap_or_else(gen_id),
1125 "permissionStatus": "DELETED",
1126 })))
1127 }
1128
1129 fn delete_permission_version(
1130 &self,
1131 req: &AwsRequest,
1132 _body: &Value,
1133 ) -> Result<AwsResponse, AwsServiceError> {
1134 let arn = query_str(req, "permissionArn")
1135 .ok_or_else(|| invalid_param("permissionArn is required."))?;
1136 let version = query_str(req, "permissionVersion")
1137 .and_then(|s| s.parse::<u32>().ok())
1138 .ok_or_else(|| invalid_param("permissionVersion is required."))?;
1139 let mut accounts = self.state.write();
1140 let st = accounts.get_or_create(&req.account_id);
1141 let p = st
1142 .permissions
1143 .get_mut(&arn)
1144 .ok_or_else(|| unknown_resource(format!("Permission {arn} could not be found.")))?;
1145 if !p.versions.iter().any(|v| v.version == version) {
1146 return Err(invalid_param(format!(
1147 "Permission version {version} could not be found."
1148 )));
1149 }
1150 if version == p.default_version {
1154 return Err(aws_err(
1155 "OperationNotPermittedException",
1156 format!("The default version {version} of a permission cannot be deleted."),
1157 ));
1158 }
1159 p.versions.retain(|v| v.version != version);
1160 let status = "ATTACHABLE";
1162 Ok(ok(json!({
1163 "returnValue": true,
1164 "clientToken": query_str(req, "clientToken").unwrap_or_else(gen_id),
1165 "permissionStatus": status,
1166 })))
1167 }
1168
1169 fn get_permission(
1170 &self,
1171 req: &AwsRequest,
1172 body: &Value,
1173 ) -> Result<AwsResponse, AwsServiceError> {
1174 let arn = req_str(body, "permissionArn")?;
1175 let now = Utc::now();
1176 if let Some((name, rt)) = find_managed(&arn) {
1177 return Ok(ok(json!({ "permission": managed_detail(name, rt, now) })));
1178 }
1179 let accounts = self.state.read();
1180 let p = accounts
1181 .get(&req.account_id)
1182 .and_then(|st| st.permissions.get(&arn))
1183 .ok_or_else(|| unknown_resource(format!("Permission {arn} could not be found.")))?;
1184 let want = u32_field(body, "permissionVersion");
1185 let v = match want {
1186 Some(n) => p.versions.iter().find(|v| v.version == n),
1187 None => p.versions.iter().find(|v| v.version == p.default_version),
1188 }
1189 .ok_or_else(|| invalid_param("Permission version could not be found."))?;
1190 Ok(ok(json!({ "permission": customer_detail(p, v) })))
1191 }
1192
1193 fn list_permissions(
1194 &self,
1195 req: &AwsRequest,
1196 body: &Value,
1197 ) -> Result<AwsResponse, AwsServiceError> {
1198 let now = Utc::now();
1199 let type_filter = str_field(body, "resourceType");
1200 let perm_type = str_field(body, "permissionType").unwrap_or_else(|| "ALL".into());
1201 let mut items: Vec<Value> = Vec::new();
1202 if perm_type == "ALL" || perm_type == "AWS_MANAGED" {
1203 for (name, rt) in MANAGED_PERMISSIONS {
1204 if type_filter.as_deref().is_none_or(|t| t == *rt) {
1205 items.push(managed_summary(name, rt, now));
1206 }
1207 }
1208 }
1209 if perm_type == "ALL" || perm_type == "CUSTOMER_MANAGED" {
1210 if let Some(st) = self.state.read().get(&req.account_id) {
1211 for p in st.permissions.values() {
1212 if type_filter.as_deref().is_none_or(|t| t == p.resource_type) {
1213 items.push(customer_summary(p));
1214 }
1215 }
1216 }
1217 }
1218 page_response(items, "permissions", body, &[])
1219 }
1220
1221 fn list_permission_versions(
1222 &self,
1223 req: &AwsRequest,
1224 body: &Value,
1225 ) -> Result<AwsResponse, AwsServiceError> {
1226 let arn = req_str(body, "permissionArn")?;
1227 let accounts = self.state.read();
1228 let p = accounts
1229 .get(&req.account_id)
1230 .and_then(|st| st.permissions.get(&arn))
1231 .ok_or_else(|| unknown_resource(format!("Permission {arn} could not be found.")))?;
1232 let items: Vec<Value> = p
1233 .versions
1234 .iter()
1235 .map(|v| {
1236 json!({
1237 "arn": p.arn,
1238 "version": v.version.to_string(),
1239 "defaultVersion": v.is_default,
1240 "name": p.name,
1241 "resourceType": p.resource_type,
1242 "status": p.status,
1243 "creationTime": ts(v.creation_time),
1244 "lastUpdatedTime": ts(v.last_updated_time),
1245 "isResourceTypeDefault": false,
1246 "permissionType": "CUSTOMER_MANAGED",
1247 "featureSet": p.feature_set,
1248 "tags": tag_list_value(&p.tags),
1249 })
1250 })
1251 .collect();
1252 page_response(items, "permissions", body, &[])
1253 }
1254
1255 fn list_resource_share_permissions(
1256 &self,
1257 req: &AwsRequest,
1258 body: &Value,
1259 ) -> Result<AwsResponse, AwsServiceError> {
1260 let arn = req_str(body, "resourceShareArn")?;
1261 let now = Utc::now();
1262 let accounts = self.state.read();
1263 let st = accounts
1264 .get(&req.account_id)
1265 .filter(|st| st.resource_shares.contains_key(&arn))
1266 .ok_or_else(|| unknown_resource(format!("ResourceShare {arn} could not be found.")))?;
1267 let share = &st.resource_shares[&arn];
1268 let mut items: Vec<Value> = Vec::new();
1269 for perm_arn in &share.permission_arns {
1270 if let Some((name, rt)) = find_managed(perm_arn) {
1271 items.push(managed_summary(name, rt, now));
1272 } else if let Some(p) = st.permissions.get(perm_arn) {
1273 items.push(customer_summary(p));
1274 }
1275 }
1276 page_response(items, "permissions", body, &[])
1277 }
1278
1279 fn set_default_permission_version(
1280 &self,
1281 req: &AwsRequest,
1282 body: &Value,
1283 ) -> Result<AwsResponse, AwsServiceError> {
1284 let arn = req_str(body, "permissionArn")?;
1285 let version = u32_field(body, "permissionVersion")
1286 .ok_or_else(|| invalid_param("permissionVersion is required."))?;
1287 let mut accounts = self.state.write();
1288 let st = accounts.get_or_create(&req.account_id);
1289 let p = st
1290 .permissions
1291 .get_mut(&arn)
1292 .ok_or_else(|| unknown_resource(format!("Permission {arn} could not be found.")))?;
1293 if !p.versions.iter().any(|v| v.version == version) {
1294 return Err(invalid_param(format!(
1295 "Permission version {version} could not be found."
1296 )));
1297 }
1298 p.default_version = version;
1299 for v in &mut p.versions {
1300 v.is_default = v.version == version;
1301 }
1302 p.last_updated_time = Utc::now();
1303 Ok(ok(json!({
1304 "returnValue": true,
1305 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
1306 })))
1307 }
1308
1309 fn promote_permission(
1310 &self,
1311 req: &AwsRequest,
1312 body: &Value,
1313 ) -> Result<AwsResponse, AwsServiceError> {
1314 let arn = req_str(body, "permissionArn")?;
1315 let _name = req_str(body, "name")?;
1316 let mut accounts = self.state.write();
1317 let st = accounts.get_or_create(&req.account_id);
1318 let p = st
1319 .permissions
1320 .get_mut(&arn)
1321 .ok_or_else(|| unknown_resource(format!("Permission {arn} could not be found.")))?;
1322 p.feature_set = "STANDARD".into();
1323 p.last_updated_time = Utc::now();
1324 Ok(ok(json!({
1325 "permission": customer_summary(p),
1326 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
1327 })))
1328 }
1329
1330 fn promote_resource_share(
1331 &self,
1332 req: &AwsRequest,
1333 body: &Value,
1334 ) -> Result<AwsResponse, AwsServiceError> {
1335 let arn = query_str(req, "resourceShareArn")
1339 .or_else(|| str_field(body, "resourceShareArn"))
1340 .ok_or_else(|| invalid_param("resourceShareArn is required."))?;
1341 let mut accounts = self.state.write();
1342 let st = accounts.get_or_create(&req.account_id);
1343 let share = st
1344 .resource_shares
1345 .get_mut(&arn)
1346 .ok_or_else(|| unknown_resource(format!("ResourceShare {arn} could not be found.")))?;
1347 share.feature_set = "STANDARD".into();
1348 share.last_updated_time = Utc::now();
1349 Ok(ok(json!({ "returnValue": true })))
1350 }
1351
1352 fn replace_permission_associations(
1353 &self,
1354 req: &AwsRequest,
1355 body: &Value,
1356 ) -> Result<AwsResponse, AwsServiceError> {
1357 let from = req_str(body, "fromPermissionArn")?;
1358 let to = req_str(body, "toPermissionArn")?;
1359 let now = Utc::now();
1360 let known = find_managed(&from).is_some()
1362 || self
1363 .state
1364 .read()
1365 .get(&req.account_id)
1366 .is_some_and(|st| st.permissions.contains_key(&from));
1367 if !known {
1368 return Err(unknown_resource(format!(
1369 "Permission {from} could not be found."
1370 )));
1371 }
1372 let from_version = u32_field(body, "fromPermissionVersion").unwrap_or(1);
1373 let id = gen_id();
1374 let work = json!({
1375 "id": id,
1376 "fromPermissionArn": from,
1377 "fromPermissionVersion": from_version.to_string(),
1378 "toPermissionArn": to,
1379 "toPermissionVersion": "1",
1380 "status": "IN_PROGRESS",
1381 "creationTime": ts(now),
1382 "lastUpdatedTime": ts(now),
1383 });
1384 let mut accounts = self.state.write();
1385 let st = accounts.get_or_create(&req.account_id);
1386 st.replace_works.insert(id, work.clone());
1387 Ok(ok(json!({
1388 "replacePermissionAssociationsWork": work,
1389 "clientToken": str_field(body, "clientToken").unwrap_or_else(gen_id),
1390 })))
1391 }
1392
1393 fn list_permission_associations(
1394 &self,
1395 req: &AwsRequest,
1396 body: &Value,
1397 ) -> Result<AwsResponse, AwsServiceError> {
1398 let perm_filter = str_field(body, "permissionArn");
1399 let version_filter = u32_field(body, "permissionVersion");
1400 let default_filter = bool_field(body, "defaultVersion");
1401 let status_filter = str_field(body, "associationStatus");
1402 let type_filter = str_field(body, "resourceType");
1403 let mut items: Vec<Value> = Vec::new();
1404 if let Some(st) = self.state.read().get(&req.account_id) {
1405 for share in st.resource_shares.values() {
1406 for perm_arn in &share.permission_arns {
1407 if perm_filter.as_deref().is_some_and(|p| p != perm_arn) {
1408 continue;
1409 }
1410 let (rtype, version, feature) = if let Some((_, rt)) = find_managed(perm_arn) {
1413 (rt.to_string(), 1u32, "STANDARD".to_string())
1414 } else if let Some(p) = st.permissions.get(perm_arn) {
1415 (
1416 p.resource_type.clone(),
1417 p.default_version,
1418 p.feature_set.clone(),
1419 )
1420 } else {
1421 ("unknown".to_string(), 1, "STANDARD".to_string())
1422 };
1423 if type_filter.as_deref().is_some_and(|t| t != rtype) {
1424 continue;
1425 }
1426 if version_filter.is_some_and(|v| v != version) {
1427 continue;
1428 }
1429 if default_filter == Some(false) {
1431 continue;
1432 }
1433 if status_filter.as_deref().is_some_and(|s| s != "ASSOCIATED") {
1434 continue;
1435 }
1436 items.push(json!({
1437 "arn": perm_arn,
1438 "permissionVersion": version.to_string(),
1439 "defaultVersion": true,
1440 "resourceType": rtype,
1441 "status": "ASSOCIATED",
1442 "featureSet": feature,
1443 "lastUpdatedTime": ts(share.last_updated_time),
1444 "resourceShareArn": share.arn,
1445 }));
1446 }
1447 }
1448 }
1449 page_response(items, "permissions", body, &[])
1450 }
1451
1452 fn list_replace_work(
1453 &self,
1454 req: &AwsRequest,
1455 body: &Value,
1456 ) -> Result<AwsResponse, AwsServiceError> {
1457 let id_filter = string_list(body, "workIds");
1458 let status_filter = str_field(body, "status");
1459 let accounts = self.state.read();
1460 let items: Vec<Value> = accounts
1461 .get(&req.account_id)
1462 .map(|st| {
1463 st.replace_works
1464 .iter()
1465 .filter(|(id, _)| id_filter.is_empty() || id_filter.contains(id))
1466 .filter(|(_, w)| {
1467 status_filter
1468 .as_deref()
1469 .is_none_or(|s| w.get("status").and_then(|v| v.as_str()) == Some(s))
1470 })
1471 .map(|(_, w)| w.clone())
1472 .collect()
1473 })
1474 .unwrap_or_default();
1475 page_response(items, "replacePermissionAssociationsWorks", body, &[])
1476 }
1477
1478 fn list_principals(
1481 &self,
1482 req: &AwsRequest,
1483 body: &Value,
1484 ) -> Result<AwsResponse, AwsServiceError> {
1485 let owner = req_str(body, "resourceOwner")?;
1486 let principal_filter = string_list(body, "principals");
1487 let arns_filter = string_list(body, "resourceShareArns");
1488 let mut items: Vec<Value> = Vec::new();
1489 if let Some(st) = self.state.read().get(&req.account_id) {
1490 for share in st.resource_shares.values() {
1491 if !owner_matches(share, &owner, &req.account_id) {
1492 continue;
1493 }
1494 if !arns_filter.is_empty() && !arns_filter.contains(&share.arn) {
1495 continue;
1496 }
1497 for a in &share.principal_associations {
1498 if !principal_filter.is_empty()
1499 && !principal_filter.contains(&a.associated_entity)
1500 {
1501 continue;
1502 }
1503 items.push(json!({
1504 "id": a.associated_entity,
1505 "resourceShareArn": share.arn,
1506 "creationTime": ts(a.creation_time),
1507 "lastUpdatedTime": ts(a.last_updated_time),
1508 "external": a.external,
1509 }));
1510 }
1511 }
1512 }
1513 page_response(items, "principals", body, &[])
1514 }
1515
1516 fn list_resources(
1517 &self,
1518 req: &AwsRequest,
1519 body: &Value,
1520 ) -> Result<AwsResponse, AwsServiceError> {
1521 let owner = req_str(body, "resourceOwner")?;
1522 let arns_filter = string_list(body, "resourceArns");
1523 let type_filter = str_field(body, "resourceType");
1524 let mut items: Vec<Value> = Vec::new();
1525 if let Some(st) = self.state.read().get(&req.account_id) {
1526 for share in st.resource_shares.values() {
1527 if !owner_matches(share, &owner, &req.account_id) {
1528 continue;
1529 }
1530 for a in &share.resource_associations {
1531 if !arns_filter.is_empty() && !arns_filter.contains(&a.associated_entity) {
1532 continue;
1533 }
1534 let rtype = ram_resource_type(&a.associated_entity);
1535 if type_filter.as_deref().is_some_and(|t| t != rtype) {
1536 continue;
1537 }
1538 items.push(json!({
1539 "arn": a.associated_entity,
1540 "type": rtype,
1541 "resourceShareArn": share.arn,
1542 "status": "AVAILABLE",
1543 "creationTime": ts(a.creation_time),
1544 "lastUpdatedTime": ts(a.last_updated_time),
1545 "resourceRegionScope": "REGIONAL",
1546 }));
1547 }
1548 }
1549 }
1550 page_response(items, "resources", body, &[])
1551 }
1552
1553 fn list_resource_types(
1554 &self,
1555 _req: &AwsRequest,
1556 body: &Value,
1557 ) -> Result<AwsResponse, AwsServiceError> {
1558 let scope_filter = str_field(body, "resourceRegionScope");
1559 let items: Vec<Value> = RESOURCE_TYPES
1560 .iter()
1561 .filter(|(_, _, scope)| match scope_filter.as_deref() {
1562 None | Some("ALL") => true,
1563 Some(s) => s == *scope,
1564 })
1565 .map(|(rt, svc, scope)| {
1566 json!({
1567 "resourceType": rt,
1568 "serviceName": svc,
1569 "resourceRegionScope": scope,
1570 })
1571 })
1572 .collect();
1573 page_response(items, "resourceTypes", body, &[])
1574 }
1575
1576 fn get_resource_policies(
1577 &self,
1578 _req: &AwsRequest,
1579 body: &Value,
1580 ) -> Result<AwsResponse, AwsServiceError> {
1581 page_response(Vec::new(), "policies", body, &[])
1583 }
1584
1585 fn tag_resource(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1588 let tags = parse_tag_list(body, "tags");
1589 let target = str_field(body, "resourceShareArn").or_else(|| str_field(body, "resourceArn"));
1590 let target = target.ok_or_else(|| {
1591 invalid_param("Either resourceShareArn or resourceArn must be specified.")
1592 })?;
1593 let mut accounts = self.state.write();
1594 let st = accounts.get_or_create(&req.account_id);
1595 if let Some(share) = st.resource_shares.get_mut(&target) {
1596 share.tags.extend(tags);
1597 } else if let Some(perm) = st.permissions.get_mut(&target) {
1598 perm.tags.extend(tags);
1599 } else {
1600 return Err(unknown_resource(format!(
1601 "Resource {target} could not be found."
1602 )));
1603 }
1604 Ok(ok(json!({})))
1605 }
1606
1607 fn untag_resource(
1608 &self,
1609 req: &AwsRequest,
1610 body: &Value,
1611 ) -> Result<AwsResponse, AwsServiceError> {
1612 let keys = string_list(body, "tagKeys");
1613 let target = str_field(body, "resourceShareArn").or_else(|| str_field(body, "resourceArn"));
1614 let target = target.ok_or_else(|| {
1615 invalid_param("Either resourceShareArn or resourceArn must be specified.")
1616 })?;
1617 let mut accounts = self.state.write();
1618 let st = accounts.get_or_create(&req.account_id);
1619 if let Some(share) = st.resource_shares.get_mut(&target) {
1620 for k in &keys {
1621 share.tags.remove(k);
1622 }
1623 } else if let Some(perm) = st.permissions.get_mut(&target) {
1624 for k in &keys {
1625 perm.tags.remove(k);
1626 }
1627 } else {
1628 return Err(unknown_resource(format!(
1629 "Resource {target} could not be found."
1630 )));
1631 }
1632 Ok(ok(json!({})))
1633 }
1634}