Skip to main content

fakecloud_cloudfront/
extras_service.rs

1//! Handlers for CloudFront Batch 6a: VPC Origins, Anycast IP Lists,
2//! Trust Stores, Resource Policies.
3
4use chrono::Utc;
5use http::{HeaderMap, StatusCode};
6
7use fakecloud_aws::arn::Arn;
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
9
10use crate::extras::{
11    CaCertificatesBundleSource, CreateAnycastIpListRequest, CreateTrustStoreRequest,
12    CreateVpcOriginRequest, ResourcePolicyRequest, StoredAnycastIpList, StoredResourcePolicy,
13    StoredTrustStore, StoredVpcOrigin, UpdateAnycastIpListRequest, VpcOriginEndpointConfig,
14};
15use crate::policies::{
16    not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
17};
18use crate::router::Route;
19use crate::service::{
20    aws_error, esc, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
21    DEFAULT_ACCOUNT,
22};
23use crate::xml_io;
24
25const NS: &str = crate::NAMESPACE;
26const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
27
28/// Convert a parsed restXml `Tags` payload into the account-keyed tag list used
29/// by ListTagsForResource. Shared by the create ops that accept create-time
30/// Tags (VPC origins, anycast IP lists, trust stores, connection groups).
31pub(crate) fn tags_to_state(tags: &Option<crate::model::Tags>) -> Vec<crate::state::Tag> {
32    tags.as_ref()
33        .and_then(|t| t.items.as_ref())
34        .map(|list| {
35            list.tag
36                .iter()
37                .map(|t| crate::state::Tag {
38                    key: t.key.clone(),
39                    value: t.value.clone(),
40                })
41                .collect()
42        })
43        .unwrap_or_default()
44}
45
46// ─── VPC Origin ───────────────────────────────────────────────────────
47
48impl CloudFrontService {
49    pub(crate) fn create_vpc_origin(
50        &self,
51        req: &AwsRequest,
52    ) -> Result<AwsResponse, AwsServiceError> {
53        let parsed: CreateVpcOriginRequest = xml_io::from_xml_root(&req.body)
54            .map_err(|e| invalid_argument(format!("invalid CreateVpcOriginRequest XML: {e}")))?;
55        let cfg = parsed.vpc_origin_endpoint_config;
56        let tags = tags_to_state(&parsed.tags);
57        if cfg.name.is_empty() {
58            return Err(invalid_argument("Name is required"));
59        }
60        if cfg.arn.is_empty() {
61            return Err(invalid_argument("Arn is required"));
62        }
63        let mut state = self.state.write();
64        let account = state
65            .accounts
66            .entry(DEFAULT_ACCOUNT.to_string())
67            .or_default();
68        if account
69            .vpc_origins
70            .values()
71            .any(|v| v.config.name == cfg.name)
72        {
73            return Err(aws_error(
74                StatusCode::CONFLICT,
75                "EntityAlreadyExists",
76                format!("VpcOrigin {} already exists", cfg.name),
77            ));
78        }
79        let id = generate_id_with_prefix("VO");
80        let etag = generate_id_with_prefix("E");
81        let now = Utc::now();
82        let arn =
83            Arn::global("cloudfront", DEFAULT_ACCOUNT, &format!("vpc-origin/{id}")).to_string();
84        let stored = StoredVpcOrigin {
85            id: id.clone(),
86            arn: arn.clone(),
87            status: "Deployed".to_string(),
88            etag: etag.clone(),
89            created_time: now,
90            last_modified_time: now,
91            config: cfg,
92        };
93        account.vpc_origins.insert(id.clone(), stored.clone());
94        if !tags.is_empty() {
95            account.tags.insert(arn, tags);
96        }
97        drop(state);
98        let body = render_vpc_origin(&stored);
99        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
100    }
101
102    pub(crate) fn get_vpc_origin(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
103        let id = route_id(route, "VpcOrigin")?;
104        let state = self.state.read();
105        let v = state
106            .accounts
107            .get(DEFAULT_ACCOUNT)
108            .and_then(|a| a.vpc_origins.get(&id).cloned())
109            .ok_or_else(|| not_found("VpcOrigin", &id))?;
110        drop(state);
111        let body = render_vpc_origin(&v);
112        Ok(xml_with_etag(StatusCode::OK, body, &v.etag, None))
113    }
114
115    pub(crate) fn update_vpc_origin(
116        &self,
117        req: &AwsRequest,
118        route: &Route,
119    ) -> Result<AwsResponse, AwsServiceError> {
120        let id = route_id(route, "VpcOrigin")?;
121        let if_match = require_if_match(req)?;
122        let cfg: VpcOriginEndpointConfig = xml_io::from_xml_root(&req.body)
123            .map_err(|e| invalid_argument(format!("invalid VpcOriginEndpointConfig XML: {e}")))?;
124        let mut state = self.state.write();
125        let account = state
126            .accounts
127            .get_mut(DEFAULT_ACCOUNT)
128            .ok_or_else(|| not_found("VpcOrigin", &id))?;
129        let v = account
130            .vpc_origins
131            .get_mut(&id)
132            .ok_or_else(|| not_found("VpcOrigin", &id))?;
133        if v.etag != if_match {
134            return Err(precondition_failed());
135        }
136        if cfg.name.is_empty() {
137            return Err(invalid_argument("Name is required"));
138        }
139        if cfg.arn.is_empty() {
140            return Err(invalid_argument("Arn is required"));
141        }
142        v.config = cfg;
143        v.etag = generate_id_with_prefix("E");
144        v.last_modified_time = Utc::now();
145        let snap = v.clone();
146        drop(state);
147        let body = render_vpc_origin(&snap);
148        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
149    }
150
151    pub(crate) fn delete_vpc_origin(
152        &self,
153        req: &AwsRequest,
154        route: &Route,
155    ) -> Result<AwsResponse, AwsServiceError> {
156        let id = route_id(route, "VpcOrigin")?;
157        let if_match = require_if_match(req)?;
158        let mut state = self.state.write();
159        let account = state
160            .accounts
161            .get_mut(DEFAULT_ACCOUNT)
162            .ok_or_else(|| not_found("VpcOrigin", &id))?;
163        let v = account
164            .vpc_origins
165            .get(&id)
166            .ok_or_else(|| not_found("VpcOrigin", &id))?;
167        if v.etag != if_match {
168            return Err(precondition_failed());
169        }
170        let arn = v.arn.clone();
171        let snap = v.clone();
172        account.vpc_origins.remove(&id);
173        account.tags.remove(&arn);
174        drop(state);
175        let body = render_vpc_origin(&snap);
176        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
177    }
178
179    pub(crate) fn list_vpc_origins(
180        &self,
181        _req: &AwsRequest,
182    ) -> Result<AwsResponse, AwsServiceError> {
183        let state = self.state.read();
184        let mut items: Vec<StoredVpcOrigin> = state
185            .accounts
186            .get(DEFAULT_ACCOUNT)
187            .map(|a| a.vpc_origins.values().cloned().collect())
188            .unwrap_or_default();
189        drop(state);
190        items.sort_by(|a, b| a.id.cmp(&b.id));
191
192        let mut body = String::with_capacity(512);
193        body.push_str(XML_DECL);
194        body.push_str(&format!("<VpcOriginList xmlns=\"{NS}\">"));
195        body.push_str("<Marker></Marker>");
196        body.push_str("<MaxItems>100</MaxItems>");
197        body.push_str(&format!("<IsTruncated>{}</IsTruncated>", false));
198        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
199        body.push_str("<Items>");
200        for v in &items {
201            body.push_str("<VpcOriginSummary>");
202            body.push_str(&format!("<Id>{}</Id>", esc(&v.id)));
203            body.push_str(&format!("<Name>{}</Name>", esc(&v.config.name)));
204            body.push_str(&format!("<Status>{}</Status>", esc(&v.status)));
205            body.push_str(&format!(
206                "<CreatedTime>{}</CreatedTime>",
207                rfc3339(&v.created_time)
208            ));
209            body.push_str(&format!(
210                "<LastModifiedTime>{}</LastModifiedTime>",
211                rfc3339(&v.last_modified_time)
212            ));
213            body.push_str(&format!("<Arn>{}</Arn>", esc(&v.arn)));
214            body.push_str(&format!(
215                "<OriginEndpointArn>{}</OriginEndpointArn>",
216                esc(&v.config.arn)
217            ));
218            body.push_str("</VpcOriginSummary>");
219        }
220        body.push_str("</Items>");
221        body.push_str("</VpcOriginList>");
222        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
223    }
224}
225
226// ─── Anycast IP List ──────────────────────────────────────────────────
227
228impl CloudFrontService {
229    pub(crate) fn create_anycast_ip_list(
230        &self,
231        req: &AwsRequest,
232    ) -> Result<AwsResponse, AwsServiceError> {
233        let cfg: CreateAnycastIpListRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
234            invalid_argument(format!("invalid CreateAnycastIpListRequest XML: {e}"))
235        })?;
236        if cfg.name.is_empty() {
237            return Err(invalid_argument("Name is required"));
238        }
239        if cfg.ip_count != 3 && cfg.ip_count != 21 {
240            return Err(invalid_argument("IpCount must be 3 or 21"));
241        }
242        let tags = tags_to_state(&cfg.tags);
243        // Retain the bring-your-own-IP IPAM CIDR configs supplied at create time
244        // so GetAnycastIpList echoes them as IpamConfig (previously dropped).
245        let ipam_cidr_configs = cfg
246            .ipam_cidr_configs
247            .as_ref()
248            .map(|l| l.ipam_cidr_config.clone())
249            .unwrap_or_default();
250        let mut state = self.state.write();
251        let account = state
252            .accounts
253            .entry(DEFAULT_ACCOUNT.to_string())
254            .or_default();
255        if account
256            .anycast_ip_lists
257            .values()
258            .any(|a| a.name == cfg.name)
259        {
260            return Err(aws_error(
261                StatusCode::CONFLICT,
262                "EntityAlreadyExists",
263                format!("AnycastIpList {} already exists", cfg.name),
264            ));
265        }
266        let id = generate_id_with_prefix("AIL");
267        let arn = format!(
268            "arn:aws:cloudfront::{}:anycast-ip-list/{}",
269            DEFAULT_ACCOUNT, id
270        );
271        // Synthesize deterministic ipv4 addresses for the list.
272        let anycast_ips: Vec<String> = (0..cfg.ip_count)
273            .map(|i| format!("198.51.100.{}", (i + 1) as u8))
274            .collect();
275        let etag = generate_id_with_prefix("E");
276        let stored = StoredAnycastIpList {
277            id: id.clone(),
278            name: cfg.name,
279            status: "Deployed".to_string(),
280            arn: arn.clone(),
281            ip_count: cfg.ip_count,
282            ip_address_type: cfg.ip_address_type,
283            anycast_ips,
284            last_modified_time: Utc::now(),
285            etag: etag.clone(),
286            ipam_cidr_configs,
287        };
288        account.anycast_ip_lists.insert(id.clone(), stored.clone());
289        if !tags.is_empty() {
290            account.tags.insert(arn, tags);
291        }
292        drop(state);
293        let body = render_anycast_ip_list(&stored);
294        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
295    }
296
297    pub(crate) fn get_anycast_ip_list(
298        &self,
299        route: &Route,
300    ) -> Result<AwsResponse, AwsServiceError> {
301        let id = route_id(route, "AnycastIpList")?;
302        let state = self.state.read();
303        let a = state
304            .accounts
305            .get(DEFAULT_ACCOUNT)
306            .and_then(|a| a.anycast_ip_lists.get(&id).cloned())
307            .ok_or_else(|| not_found("AnycastIpList", &id))?;
308        drop(state);
309        let body = render_anycast_ip_list(&a);
310        Ok(xml_with_etag(StatusCode::OK, body, &a.etag, None))
311    }
312
313    pub(crate) fn update_anycast_ip_list(
314        &self,
315        req: &AwsRequest,
316        route: &Route,
317    ) -> Result<AwsResponse, AwsServiceError> {
318        let id = route_id(route, "AnycastIpList")?;
319        let if_match = require_if_match(req)?;
320        let cfg: UpdateAnycastIpListRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
321            invalid_argument(format!("invalid UpdateAnycastIpListRequest XML: {e}"))
322        })?;
323        let mut state = self.state.write();
324        let account = state
325            .accounts
326            .get_mut(DEFAULT_ACCOUNT)
327            .ok_or_else(|| not_found("AnycastIpList", &id))?;
328        let a = account
329            .anycast_ip_lists
330            .get_mut(&id)
331            .ok_or_else(|| not_found("AnycastIpList", &id))?;
332        if a.etag != if_match {
333            return Err(precondition_failed());
334        }
335        if let Some(t) = cfg.ip_address_type {
336            a.ip_address_type = Some(t);
337        }
338        if let Some(l) = cfg.ipam_cidr_configs {
339            a.ipam_cidr_configs = l.ipam_cidr_config;
340        }
341        a.last_modified_time = Utc::now();
342        a.etag = generate_id_with_prefix("E");
343        let snap = a.clone();
344        drop(state);
345        let body = render_anycast_ip_list(&snap);
346        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
347    }
348
349    pub(crate) fn delete_anycast_ip_list(
350        &self,
351        req: &AwsRequest,
352        route: &Route,
353    ) -> Result<AwsResponse, AwsServiceError> {
354        let id = route_id(route, "AnycastIpList")?;
355        let if_match = require_if_match(req)?;
356        let mut state = self.state.write();
357        let account = state
358            .accounts
359            .get_mut(DEFAULT_ACCOUNT)
360            .ok_or_else(|| not_found("AnycastIpList", &id))?;
361        let a = account
362            .anycast_ip_lists
363            .get(&id)
364            .ok_or_else(|| not_found("AnycastIpList", &id))?;
365        if a.etag != if_match {
366            return Err(precondition_failed());
367        }
368        let arn = a.arn.clone();
369        account.anycast_ip_lists.remove(&id);
370        account.tags.remove(&arn);
371        drop(state);
372        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
373    }
374
375    pub(crate) fn list_anycast_ip_lists(
376        &self,
377        _req: &AwsRequest,
378    ) -> Result<AwsResponse, AwsServiceError> {
379        let state = self.state.read();
380        let mut items: Vec<StoredAnycastIpList> = state
381            .accounts
382            .get(DEFAULT_ACCOUNT)
383            .map(|a| a.anycast_ip_lists.values().cloned().collect())
384            .unwrap_or_default();
385        drop(state);
386        items.sort_by(|a, b| a.id.cmp(&b.id));
387
388        let mut body = String::with_capacity(512);
389        body.push_str(XML_DECL);
390        body.push_str(&format!("<AnycastIpListCollection xmlns=\"{NS}\">"));
391        body.push_str("<Marker></Marker>");
392        body.push_str("<MaxItems>100</MaxItems>");
393        body.push_str(&format!("<IsTruncated>{}</IsTruncated>", false));
394        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
395        body.push_str("<Items>");
396        for a in &items {
397            body.push_str("<AnycastIpListSummary>");
398            push_anycast_summary(&mut body, a);
399            body.push_str("</AnycastIpListSummary>");
400        }
401        body.push_str("</Items>");
402        body.push_str("</AnycastIpListCollection>");
403        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
404    }
405}
406
407// ─── Trust Store ──────────────────────────────────────────────────────
408
409impl CloudFrontService {
410    pub(crate) fn create_trust_store(
411        &self,
412        req: &AwsRequest,
413    ) -> Result<AwsResponse, AwsServiceError> {
414        let cfg: CreateTrustStoreRequest = xml_io::from_xml_root(&req.body)
415            .map_err(|e| invalid_argument(format!("invalid CreateTrustStoreRequest XML: {e}")))?;
416        if cfg.name.is_empty() {
417            return Err(invalid_argument("Name is required"));
418        }
419        if cfg
420            .ca_certificates_bundle_source
421            .ca_certificates_bundle_s3_location
422            .is_none()
423        {
424            return Err(invalid_argument(
425                "CaCertificatesBundleSource must specify a non-empty member",
426            ));
427        }
428        let mut state = self.state.write();
429        let account = state
430            .accounts
431            .entry(DEFAULT_ACCOUNT.to_string())
432            .or_default();
433        if account.trust_stores.values().any(|t| t.name == cfg.name) {
434            return Err(aws_error(
435                StatusCode::CONFLICT,
436                "EntityAlreadyExists",
437                format!("TrustStore {} already exists", cfg.name),
438            ));
439        }
440        let id = generate_id_with_prefix("TS");
441        let arn =
442            Arn::global("cloudfront", DEFAULT_ACCOUNT, &format!("trust-store/{id}")).to_string();
443        let etag = generate_id_with_prefix("E");
444        let tags = tags_to_state(&cfg.tags);
445        let stored = StoredTrustStore {
446            id: id.clone(),
447            arn: arn.clone(),
448            name: cfg.name,
449            etag: etag.clone(),
450            last_modified_time: Utc::now(),
451            ca_certificates_bundle_source: cfg.ca_certificates_bundle_source,
452            use_client_certificate_ocsp_endpoint: cfg.use_client_certificate_ocsp_endpoint,
453        };
454        account.trust_stores.insert(id.clone(), stored.clone());
455        if !tags.is_empty() {
456            account.tags.insert(arn, tags);
457        }
458        drop(state);
459        let body = render_trust_store(&stored);
460        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
461    }
462
463    pub(crate) fn get_trust_store(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
464        let id = route_id(route, "TrustStore")?;
465        let state = self.state.read();
466        let t = state
467            .accounts
468            .get(DEFAULT_ACCOUNT)
469            .and_then(|a| a.trust_stores.get(&id).cloned())
470            .ok_or_else(|| not_found("TrustStore", &id))?;
471        drop(state);
472        let body = render_trust_store(&t);
473        Ok(xml_with_etag(StatusCode::OK, body, &t.etag, None))
474    }
475
476    pub(crate) fn update_trust_store(
477        &self,
478        req: &AwsRequest,
479        route: &Route,
480    ) -> Result<AwsResponse, AwsServiceError> {
481        let id = route_id(route, "TrustStore")?;
482        let if_match = require_if_match(req)?;
483        let bundle: CaCertificatesBundleSource = xml_io::from_xml_root(&req.body).map_err(|e| {
484            invalid_argument(format!("invalid CaCertificatesBundleSource XML: {e}"))
485        })?;
486        if bundle.ca_certificates_bundle_s3_location.is_none() {
487            return Err(invalid_argument(
488                "CaCertificatesBundleSource must specify a non-empty member",
489            ));
490        }
491        let mut state = self.state.write();
492        let account = state
493            .accounts
494            .get_mut(DEFAULT_ACCOUNT)
495            .ok_or_else(|| not_found("TrustStore", &id))?;
496        let t = account
497            .trust_stores
498            .get_mut(&id)
499            .ok_or_else(|| not_found("TrustStore", &id))?;
500        if t.etag != if_match {
501            return Err(precondition_failed());
502        }
503        t.ca_certificates_bundle_source = bundle;
504        t.last_modified_time = Utc::now();
505        t.etag = generate_id_with_prefix("E");
506        let snap = t.clone();
507        drop(state);
508        let body = render_trust_store(&snap);
509        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
510    }
511
512    pub(crate) fn delete_trust_store(
513        &self,
514        req: &AwsRequest,
515        route: &Route,
516    ) -> Result<AwsResponse, AwsServiceError> {
517        let id = route_id(route, "TrustStore")?;
518        let if_match = require_if_match(req)?;
519        let mut state = self.state.write();
520        let account = state
521            .accounts
522            .get_mut(DEFAULT_ACCOUNT)
523            .ok_or_else(|| not_found("TrustStore", &id))?;
524        let t = account
525            .trust_stores
526            .get(&id)
527            .ok_or_else(|| not_found("TrustStore", &id))?;
528        if t.etag != if_match {
529            return Err(precondition_failed());
530        }
531        let arn = t.arn.clone();
532        account.trust_stores.remove(&id);
533        account.tags.remove(&arn);
534        drop(state);
535        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
536    }
537
538    pub(crate) fn list_trust_stores(
539        &self,
540        _req: &AwsRequest,
541    ) -> Result<AwsResponse, AwsServiceError> {
542        let state = self.state.read();
543        let mut items: Vec<StoredTrustStore> = state
544            .accounts
545            .get(DEFAULT_ACCOUNT)
546            .map(|a| a.trust_stores.values().cloned().collect())
547            .unwrap_or_default();
548        drop(state);
549        items.sort_by(|a, b| a.id.cmp(&b.id));
550
551        let mut body = String::with_capacity(512);
552        body.push_str(XML_DECL);
553        body.push_str(&format!("<ListTrustStoresResult xmlns=\"{NS}\">"));
554        body.push_str("<TrustStoreList>");
555        for t in &items {
556            body.push_str("<TrustStoreSummary>");
557            body.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
558            body.push_str(&format!("<Arn>{}</Arn>", esc(&t.arn)));
559            body.push_str(&format!("<Name>{}</Name>", esc(&t.name)));
560            body.push_str(&format!(
561                "<LastModifiedTime>{}</LastModifiedTime>",
562                rfc3339(&t.last_modified_time)
563            ));
564            body.push_str("</TrustStoreSummary>");
565        }
566        body.push_str("</TrustStoreList>");
567        body.push_str("</ListTrustStoresResult>");
568        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
569    }
570}
571
572// ─── Resource Policy ──────────────────────────────────────────────────
573
574impl CloudFrontService {
575    pub(crate) fn put_resource_policy(
576        &self,
577        req: &AwsRequest,
578    ) -> Result<AwsResponse, AwsServiceError> {
579        let parsed: ResourcePolicyRequest = xml_io::from_xml_root(&req.body)
580            .map_err(|e| invalid_argument(format!("invalid PutResourcePolicyRequest XML: {e}")))?;
581        if parsed.resource_arn.is_empty() {
582            return Err(invalid_argument("ResourceArn is required"));
583        }
584        let policy = parsed
585            .policy_document
586            .ok_or_else(|| invalid_argument("PolicyDocument is required"))?;
587        let mut state = self.state.write();
588        let account = state
589            .accounts
590            .entry(DEFAULT_ACCOUNT.to_string())
591            .or_default();
592        account.resource_policies.insert(
593            parsed.resource_arn.clone(),
594            StoredResourcePolicy {
595                resource_arn: parsed.resource_arn,
596                policy_document: policy,
597            },
598        );
599        drop(state);
600        let mut body = String::with_capacity(128);
601        body.push_str(XML_DECL);
602        body.push_str(&format!("<PutResourcePolicyResult xmlns=\"{NS}\"/>"));
603        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
604    }
605
606    pub(crate) fn get_resource_policy(
607        &self,
608        req: &AwsRequest,
609    ) -> Result<AwsResponse, AwsServiceError> {
610        let parsed: ResourcePolicyRequest = xml_io::from_xml_root(&req.body)
611            .map_err(|e| invalid_argument(format!("invalid GetResourcePolicyRequest XML: {e}")))?;
612        if parsed.resource_arn.is_empty() {
613            return Err(invalid_argument("ResourceArn is required"));
614        }
615        let state = self.state.read();
616        let p = state
617            .accounts
618            .get(DEFAULT_ACCOUNT)
619            .and_then(|a| a.resource_policies.get(&parsed.resource_arn).cloned())
620            .ok_or_else(|| not_found("ResourcePolicy", &parsed.resource_arn))?;
621        drop(state);
622        let mut body = String::with_capacity(512);
623        body.push_str(XML_DECL);
624        body.push_str(&format!("<GetResourcePolicyResult xmlns=\"{NS}\">"));
625        body.push_str(&format!(
626            "<ResourceArn>{}</ResourceArn>",
627            esc(&p.resource_arn)
628        ));
629        body.push_str(&format!(
630            "<PolicyDocument>{}</PolicyDocument>",
631            esc(&p.policy_document)
632        ));
633        body.push_str("</GetResourcePolicyResult>");
634        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
635    }
636
637    pub(crate) fn delete_resource_policy(
638        &self,
639        req: &AwsRequest,
640    ) -> Result<AwsResponse, AwsServiceError> {
641        let parsed: ResourcePolicyRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
642            invalid_argument(format!("invalid DeleteResourcePolicyRequest XML: {e}"))
643        })?;
644        if parsed.resource_arn.is_empty() {
645            return Err(invalid_argument("ResourceArn is required"));
646        }
647        let mut state = self.state.write();
648        let account = state
649            .accounts
650            .get_mut(DEFAULT_ACCOUNT)
651            .ok_or_else(|| not_found("ResourcePolicy", &parsed.resource_arn))?;
652        if account
653            .resource_policies
654            .remove(&parsed.resource_arn)
655            .is_none()
656        {
657            return Err(not_found("ResourcePolicy", &parsed.resource_arn));
658        }
659        drop(state);
660        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
661    }
662}
663
664// ─── XML render helpers ───────────────────────────────────────────────
665
666fn render_vpc_origin(v: &StoredVpcOrigin) -> String {
667    let mut out = String::with_capacity(512);
668    out.push_str(XML_DECL);
669    out.push_str(&format!("<VpcOrigin xmlns=\"{NS}\">"));
670    out.push_str(&format!("<Id>{}</Id>", esc(&v.id)));
671    out.push_str(&format!("<Arn>{}</Arn>", esc(&v.arn)));
672    out.push_str(&format!("<Status>{}</Status>", esc(&v.status)));
673    out.push_str(&format!(
674        "<CreatedTime>{}</CreatedTime>",
675        rfc3339(&v.created_time)
676    ));
677    out.push_str(&format!(
678        "<LastModifiedTime>{}</LastModifiedTime>",
679        rfc3339(&v.last_modified_time)
680    ));
681    out.push_str(&render_vpc_origin_endpoint_config(&v.config));
682    out.push_str("</VpcOrigin>");
683    out
684}
685
686fn render_vpc_origin_endpoint_config(c: &VpcOriginEndpointConfig) -> String {
687    let mut out = String::with_capacity(256);
688    out.push_str("<VpcOriginEndpointConfig>");
689    out.push_str(&format!("<Name>{}</Name>", esc(&c.name)));
690    out.push_str(&format!("<Arn>{}</Arn>", esc(&c.arn)));
691    out.push_str(&format!("<HTTPPort>{}</HTTPPort>", c.http_port));
692    out.push_str(&format!("<HTTPSPort>{}</HTTPSPort>", c.https_port));
693    out.push_str(&format!(
694        "<OriginProtocolPolicy>{}</OriginProtocolPolicy>",
695        esc(&c.origin_protocol_policy)
696    ));
697    if let Some(ssl) = &c.origin_ssl_protocols {
698        out.push_str("<OriginSslProtocols>");
699        out.push_str(&format!("<Quantity>{}</Quantity>", ssl.quantity));
700        out.push_str("<Items>");
701        for p in &ssl.items.ssl_protocol {
702            out.push_str(&format!("<SslProtocol>{}</SslProtocol>", esc(p)));
703        }
704        out.push_str("</Items>");
705        out.push_str("</OriginSslProtocols>");
706    }
707    out.push_str("</VpcOriginEndpointConfig>");
708    out
709}
710
711fn render_anycast_ip_list(a: &StoredAnycastIpList) -> String {
712    let mut out = String::with_capacity(512);
713    out.push_str(XML_DECL);
714    out.push_str(&format!("<AnycastIpList xmlns=\"{NS}\">"));
715    out.push_str(&format!("<Id>{}</Id>", esc(&a.id)));
716    out.push_str(&format!("<Name>{}</Name>", esc(&a.name)));
717    out.push_str(&format!("<Status>{}</Status>", esc(&a.status)));
718    out.push_str(&format!("<Arn>{}</Arn>", esc(&a.arn)));
719    if let Some(t) = &a.ip_address_type {
720        out.push_str(&format!("<IpAddressType>{}</IpAddressType>", esc(t)));
721    }
722    if !a.ipam_cidr_configs.is_empty() {
723        out.push_str("<IpamConfig>");
724        out.push_str(&format!(
725            "<Quantity>{}</Quantity>",
726            a.ipam_cidr_configs.len()
727        ));
728        out.push_str("<IpamCidrConfigs>");
729        for c in &a.ipam_cidr_configs {
730            out.push_str("<IpamCidrConfig>");
731            out.push_str(&format!("<Cidr>{}</Cidr>", esc(&c.cidr)));
732            out.push_str(&format!(
733                "<IpamPoolArn>{}</IpamPoolArn>",
734                esc(&c.ipam_pool_arn)
735            ));
736            if let Some(ip) = &c.anycast_ip {
737                out.push_str(&format!("<AnycastIp>{}</AnycastIp>", esc(ip)));
738            }
739            if let Some(s) = &c.status {
740                out.push_str(&format!("<Status>{}</Status>", esc(s)));
741            }
742            out.push_str("</IpamCidrConfig>");
743        }
744        out.push_str("</IpamCidrConfigs>");
745        out.push_str("</IpamConfig>");
746    }
747    out.push_str("<AnycastIps>");
748    for ip in &a.anycast_ips {
749        out.push_str(&format!("<AnycastIp>{}</AnycastIp>", esc(ip)));
750    }
751    out.push_str("</AnycastIps>");
752    out.push_str(&format!("<IpCount>{}</IpCount>", a.ip_count));
753    out.push_str(&format!(
754        "<LastModifiedTime>{}</LastModifiedTime>",
755        rfc3339(&a.last_modified_time)
756    ));
757    out.push_str("</AnycastIpList>");
758    out
759}
760
761fn push_anycast_summary(out: &mut String, a: &StoredAnycastIpList) {
762    out.push_str(&format!("<Id>{}</Id>", esc(&a.id)));
763    out.push_str(&format!("<Name>{}</Name>", esc(&a.name)));
764    out.push_str(&format!("<Status>{}</Status>", esc(&a.status)));
765    out.push_str(&format!("<Arn>{}</Arn>", esc(&a.arn)));
766    if let Some(t) = &a.ip_address_type {
767        out.push_str(&format!("<IpAddressType>{}</IpAddressType>", esc(t)));
768    }
769    out.push_str(&format!("<IpCount>{}</IpCount>", a.ip_count));
770    out.push_str(&format!(
771        "<LastModifiedTime>{}</LastModifiedTime>",
772        rfc3339(&a.last_modified_time)
773    ));
774}
775
776fn render_trust_store(t: &StoredTrustStore) -> String {
777    let mut out = String::with_capacity(512);
778    out.push_str(XML_DECL);
779    out.push_str(&format!("<TrustStore xmlns=\"{NS}\">"));
780    out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
781    out.push_str(&format!("<Arn>{}</Arn>", esc(&t.arn)));
782    out.push_str(&format!("<Name>{}</Name>", esc(&t.name)));
783    out.push_str(&format!(
784        "<LastModifiedTime>{}</LastModifiedTime>",
785        rfc3339(&t.last_modified_time)
786    ));
787    out.push_str(&render_bundle_source(&t.ca_certificates_bundle_source));
788    if let Some(ocsp) = t.use_client_certificate_ocsp_endpoint {
789        out.push_str(&format!(
790            "<UseClientCertificateOCSPEndpoint>{ocsp}</UseClientCertificateOCSPEndpoint>"
791        ));
792    }
793    out.push_str("</TrustStore>");
794    out
795}
796
797fn render_bundle_source(s: &CaCertificatesBundleSource) -> String {
798    let mut out = String::with_capacity(256);
799    out.push_str("<CaCertificatesBundleSource>");
800    if let Some(s3) = &s.ca_certificates_bundle_s3_location {
801        out.push_str("<CaCertificatesBundleS3Location>");
802        out.push_str(&format!("<Bucket>{}</Bucket>", esc(&s3.bucket)));
803        out.push_str(&format!("<Key>{}</Key>", esc(&s3.key)));
804        out.push_str(&format!("<Region>{}</Region>", esc(&s3.region)));
805        if let Some(v) = &s3.version {
806            out.push_str(&format!("<Version>{}</Version>", esc(v)));
807        }
808        out.push_str("</CaCertificatesBundleS3Location>");
809    }
810    out.push_str("</CaCertificatesBundleSource>");
811    out
812}
813
814#[cfg(test)]
815mod extras_create_tests {
816    use super::*;
817    use crate::state::CloudFrontAccounts;
818    use fakecloud_core::service::{AwsService, ResponseBody};
819    use parking_lot::RwLock;
820    use std::sync::Arc;
821
822    fn svc() -> CloudFrontService {
823        CloudFrontService::new(Arc::new(RwLock::new(CloudFrontAccounts::new())))
824    }
825
826    fn req(method: http::Method, path: &str, raw_query: &str, body: &str) -> AwsRequest {
827        AwsRequest {
828            service: "cloudfront".into(),
829            action: String::new(),
830            region: "us-east-1".into(),
831            account_id: DEFAULT_ACCOUNT.into(),
832            request_id: "t".into(),
833            headers: HeaderMap::new(),
834            query_params: std::collections::HashMap::new(),
835            body_stream: parking_lot::Mutex::new(None),
836            body: bytes::Bytes::from(body.to_string()),
837            path_segments: vec![],
838            raw_path: path.into(),
839            raw_query: raw_query.into(),
840            method,
841            is_query_protocol: false,
842            access_key_id: None,
843            principal: None,
844        }
845    }
846
847    fn body_str(resp: &AwsResponse) -> String {
848        match &resp.body {
849            ResponseBody::Bytes(b) => String::from_utf8(b.to_vec()).unwrap(),
850            _ => panic!("expected bytes body"),
851        }
852    }
853
854    fn between<'a>(xml: &'a str, tag: &str) -> Option<&'a str> {
855        xml.split(&format!("<{tag}>"))
856            .nth(1)
857            .and_then(|s| s.split(&format!("</{tag}>")).next())
858    }
859
860    #[tokio::test]
861    async fn create_trust_store_persists_ocsp_and_tags() {
862        let s = svc();
863        let body = format!(
864            r#"<?xml version="1.0"?>
865<CreateTrustStoreRequest xmlns="{NS}">
866  <Name>ts1</Name>
867  <CaCertificatesBundleSource>
868    <CaCertificatesBundleS3Location><Bucket>b</Bucket><Key>k</Key><Region>us-east-1</Region></CaCertificatesBundleS3Location>
869  </CaCertificatesBundleSource>
870  <UseClientCertificateOCSPEndpoint>true</UseClientCertificateOCSPEndpoint>
871  <Tags><Items><Tag><Key>team</Key><Value>edge</Value></Tag></Items></Tags>
872</CreateTrustStoreRequest>"#
873        );
874        let created = s
875            .handle(req(
876                http::Method::POST,
877                "/2020-05-31/trust-store",
878                "",
879                &body,
880            ))
881            .await
882            .unwrap();
883        let xml = body_str(&created);
884        let id = between(&xml, "Id").unwrap().to_string();
885        let arn = between(&xml, "Arn").unwrap().to_string();
886
887        // GetTrustStore echoes the OCSP flag.
888        let got = s
889            .handle(req(
890                http::Method::GET,
891                &format!("/2020-05-31/trust-store/{id}"),
892                "",
893                "",
894            ))
895            .await
896            .unwrap();
897        let gxml = body_str(&got);
898        assert_eq!(
899            between(&gxml, "UseClientCertificateOCSPEndpoint"),
900            Some("true"),
901            "OCSP echoed: {gxml}"
902        );
903
904        // Create-time tags are visible via ListTagsForResource.
905        let tags = s
906            .handle(req(
907                http::Method::GET,
908                "/2020-05-31/tagging",
909                &format!("Resource={arn}"),
910                "",
911            ))
912            .await
913            .unwrap();
914        let txml = body_str(&tags);
915        assert!(txml.contains("<Key>team</Key>"), "tag listed: {txml}");
916        assert!(txml.contains("<Value>edge</Value>"));
917    }
918
919    #[tokio::test]
920    async fn create_anycast_ip_list_persists_ipam_and_tags() {
921        let s = svc();
922        let body = format!(
923            r#"<?xml version="1.0"?>
924<CreateAnycastIpListRequest xmlns="{NS}">
925  <Name>ail1</Name>
926  <IpCount>3</IpCount>
927  <IpamCidrConfigs>
928    <IpamCidrConfig>
929      <Cidr>10.0.0.0/24</Cidr>
930      <IpamPoolArn>arn:aws:ec2::123456789012:ipam-pool/ipam-pool-1</IpamPoolArn>
931    </IpamCidrConfig>
932  </IpamCidrConfigs>
933  <Tags><Items><Tag><Key>env</Key><Value>prod</Value></Tag></Items></Tags>
934</CreateAnycastIpListRequest>"#
935        );
936        let created = s
937            .handle(req(
938                http::Method::POST,
939                "/2020-05-31/anycast-ip-list",
940                "",
941                &body,
942            ))
943            .await
944            .unwrap();
945        let xml = body_str(&created);
946        let id = between(&xml, "Id").unwrap().to_string();
947        let arn = between(&xml, "Arn").unwrap().to_string();
948
949        let got = s
950            .handle(req(
951                http::Method::GET,
952                &format!("/2020-05-31/anycast-ip-list/{id}"),
953                "",
954                "",
955            ))
956            .await
957            .unwrap();
958        let gxml = body_str(&got);
959        assert!(gxml.contains("<IpamConfig>"), "IpamConfig echoed: {gxml}");
960        assert!(gxml.contains("<Cidr>10.0.0.0/24</Cidr>"));
961        assert!(gxml.contains("ipam-pool-1"));
962
963        let tags = s
964            .handle(req(
965                http::Method::GET,
966                "/2020-05-31/tagging",
967                &format!("Resource={arn}"),
968                "",
969            ))
970            .await
971            .unwrap();
972        assert!(body_str(&tags).contains("<Key>env</Key>"));
973    }
974}