1use 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
28pub(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
46impl 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
226impl 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 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 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
407impl 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
572impl 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
664fn 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 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 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}