1use chrono::Utc;
6use http::header::LOCATION;
7use http::{HeaderMap, StatusCode};
8use uuid::Uuid;
9
10use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
11
12use crate::policies::{
13 not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
14};
15use crate::router::Route;
16use crate::service::{
17 aws_error, esc, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
18 DEFAULT_ACCOUNT,
19};
20use crate::state::Tag;
21use crate::tenants::{
22 StoredDistributionTenant, StoredTenantInvalidation, TenantCustomizations,
23 TenantGeoRestrictionCustomization, TenantParameter, TenantWebAclCustomization,
24};
25use crate::xml_io;
26
27const NS: &str = crate::NAMESPACE;
28const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
29
30#[derive(Debug, Default, serde::Deserialize)]
31#[serde(rename_all = "PascalCase")]
32struct CreateDistributionTenantRequest {
33 pub distribution_id: String,
34 pub name: String,
35 #[serde(default)]
36 pub domains: Option<DomainItems>,
37 #[serde(default)]
38 pub connection_group_id: Option<String>,
39 #[serde(default)]
40 pub enabled: Option<bool>,
41 #[serde(default)]
42 pub parameters: Option<ParametersReq>,
43 #[serde(default)]
44 pub customizations: Option<CustomizationsReq>,
45 #[serde(default)]
49 pub tags: Option<TagsReq>,
50}
51
52#[derive(Debug, Default, serde::Deserialize)]
53#[serde(rename_all = "PascalCase")]
54struct UpdateDistributionTenantRequest {
55 #[serde(default)]
56 pub distribution_id: Option<String>,
57 #[serde(default)]
58 pub domains: Option<DomainItems>,
59 #[serde(default)]
60 pub connection_group_id: Option<String>,
61 #[serde(default)]
62 pub enabled: Option<bool>,
63 #[serde(default)]
64 pub parameters: Option<ParametersReq>,
65 #[serde(default)]
66 pub customizations: Option<CustomizationsReq>,
67}
68
69#[derive(Debug, Default, serde::Deserialize)]
70#[serde(rename_all = "PascalCase")]
71struct ParametersReq {
72 #[serde(default, rename = "Parameter")]
73 pub parameter: Vec<ParameterReq>,
74}
75
76#[derive(Debug, Default, serde::Deserialize)]
77#[serde(rename_all = "PascalCase")]
78struct ParameterReq {
79 pub name: String,
80 #[serde(default)]
81 pub value: String,
82}
83
84#[derive(Debug, Default, serde::Deserialize)]
85#[serde(rename_all = "PascalCase")]
86struct CustomizationsReq {
87 #[serde(default)]
88 pub web_acl: Option<WebAclReq>,
89 #[serde(default)]
90 pub certificate: Option<CertificateReq>,
91 #[serde(default)]
92 pub geo_restrictions: Option<GeoRestrictionsReq>,
93}
94
95#[derive(Debug, Default, serde::Deserialize)]
96#[serde(rename_all = "PascalCase")]
97struct WebAclReq {
98 #[serde(default)]
99 pub action: String,
100 #[serde(default, rename = "Arn")]
101 pub arn: Option<String>,
102}
103
104#[derive(Debug, Default, serde::Deserialize)]
105#[serde(rename_all = "PascalCase")]
106struct CertificateReq {
107 #[serde(rename = "Arn")]
108 pub arn: String,
109}
110
111#[derive(Debug, Default, serde::Deserialize)]
112#[serde(rename_all = "PascalCase")]
113struct GeoRestrictionsReq {
114 #[serde(default)]
115 pub restriction_type: String,
116 #[serde(default)]
117 pub locations: Option<LocationsReq>,
118}
119
120#[derive(Debug, Default, serde::Deserialize)]
121#[serde(rename_all = "PascalCase")]
122struct LocationsReq {
123 #[serde(default, rename = "Location")]
124 pub location: Vec<String>,
125}
126
127#[derive(Debug, Default, serde::Deserialize)]
128#[serde(rename_all = "PascalCase")]
129struct TagsReq {
130 #[serde(default)]
131 pub items: Option<TagItemsReq>,
132}
133
134#[derive(Debug, Default, serde::Deserialize)]
135#[serde(rename_all = "PascalCase")]
136struct TagItemsReq {
137 #[serde(default, rename = "Tag")]
138 pub tag: Vec<TagReq>,
139}
140
141#[derive(Debug, Default, serde::Deserialize)]
142#[serde(rename_all = "PascalCase")]
143struct TagReq {
144 pub key: String,
145 #[serde(default)]
146 pub value: Option<String>,
147}
148
149#[derive(Debug, Default, serde::Deserialize)]
150struct DomainItems {
151 #[serde(default, rename = "member")]
152 pub members: Vec<DomainItem>,
153}
154
155#[derive(Debug, Default, serde::Deserialize)]
156#[serde(rename_all = "PascalCase")]
157struct DomainItem {
158 pub domain: String,
159}
160
161#[derive(Debug, Default, serde::Deserialize)]
162struct AssociateWebAclRequest {
163 #[serde(rename = "WebACLArn")]
164 pub web_acl_arn: String,
165}
166
167#[derive(Debug, Default, serde::Deserialize)]
168#[serde(rename_all = "PascalCase")]
169struct InvalidationBatchRequest {
170 pub paths: PathsItems,
171 pub caller_reference: String,
172}
173
174#[derive(Debug, Default, serde::Deserialize)]
175#[serde(rename_all = "PascalCase")]
176struct PathsItems {
177 #[serde(default)]
178 pub items: Option<PathItems>,
179}
180
181#[derive(Debug, Default, serde::Deserialize)]
182#[serde(rename_all = "PascalCase")]
183struct PathItems {
184 #[serde(default, rename = "Path")]
185 pub path: Vec<String>,
186}
187
188impl CloudFrontService {
189 pub(crate) fn create_distribution_tenant(
190 &self,
191 req: &AwsRequest,
192 ) -> Result<AwsResponse, AwsServiceError> {
193 let parsed: CreateDistributionTenantRequest =
194 xml_io::from_xml_root(&req.body).map_err(|e| {
195 invalid_argument(format!("invalid CreateDistributionTenantRequest XML: {e}"))
196 })?;
197 if parsed.distribution_id.is_empty() {
198 return Err(invalid_argument("DistributionId is required"));
199 }
200 if parsed.name.is_empty() {
201 return Err(invalid_argument("Name is required"));
202 }
203 let mut state = self.state.write();
204 let account = state
205 .accounts
206 .entry(DEFAULT_ACCOUNT.to_string())
207 .or_default();
208 if account
209 .distribution_tenants
210 .values()
211 .any(|t| t.name == parsed.name)
212 {
213 return Err(aws_error(
214 StatusCode::CONFLICT,
215 "EntityAlreadyExists",
216 format!("DistributionTenant {} already exists", parsed.name),
217 ));
218 }
219 let id = generate_tenant_id();
220 let arn = format!(
221 "arn:aws:cloudfront::{}:distribution-tenant/{}",
222 DEFAULT_ACCOUNT, id
223 );
224 let etag = generate_id_with_prefix("E");
225 let now = Utc::now();
226 let domains = parsed
227 .domains
228 .map(|d| d.members.into_iter().map(|i| i.domain).collect())
229 .unwrap_or_default();
230 let customizations = parsed.customizations.map(convert_customizations);
231 let web_acl_arn = customizations
232 .as_ref()
233 .and_then(|c| c.web_acl.as_ref())
234 .and_then(|w| w.arn.clone());
235 let stored = StoredDistributionTenant {
236 id: id.clone(),
237 arn: arn.clone(),
238 name: parsed.name,
239 distribution_id: parsed.distribution_id,
240 domains,
241 connection_group_id: parsed.connection_group_id,
242 web_acl_arn,
243 enabled: parsed.enabled.unwrap_or(true),
244 status: "InProgress".to_string(),
245 etag: etag.clone(),
246 created_time: now,
247 last_modified_time: now,
248 parameters: convert_parameters(parsed.parameters),
249 customizations,
250 };
251 account
252 .distribution_tenants
253 .insert(id.clone(), stored.clone());
254 if let Some(tags) = parsed.tags {
255 let converted: Vec<Tag> = tags
256 .items
257 .map(|i| {
258 i.tag
259 .into_iter()
260 .map(|t| Tag {
261 key: t.key,
262 value: t.value,
263 })
264 .collect()
265 })
266 .unwrap_or_default();
267 if !converted.is_empty() {
268 account.tags.entry(arn).or_default().extend(converted);
269 }
270 }
271 drop(state);
272 self.schedule_distribution_tenant_deploy(id.clone());
273 let body = render_distribution_tenant(&stored);
274 Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
275 }
276
277 pub(crate) fn get_distribution_tenant(
278 &self,
279 route: &Route,
280 ) -> Result<AwsResponse, AwsServiceError> {
281 let id = route_id(route, "DistributionTenant")?;
282 let state = self.state.read();
283 let t = state
284 .accounts
285 .get(DEFAULT_ACCOUNT)
286 .and_then(|a| a.distribution_tenants.get(&id).cloned())
287 .ok_or_else(|| not_found("DistributionTenant", &id))?;
288 drop(state);
289 let body = render_distribution_tenant(&t);
290 Ok(xml_with_etag(StatusCode::OK, body, &t.etag, None))
291 }
292
293 pub(crate) fn get_distribution_tenant_by_domain(
294 &self,
295 req: &AwsRequest,
296 ) -> Result<AwsResponse, AwsServiceError> {
297 let domain = req
298 .query_params
299 .get("domain")
300 .or_else(|| req.query_params.get("Domain"))
301 .cloned()
302 .ok_or_else(|| invalid_argument("Domain query parameter is required"))?;
303 let state = self.state.read();
304 let t = state
305 .accounts
306 .get(DEFAULT_ACCOUNT)
307 .and_then(|a| {
308 a.distribution_tenants
309 .values()
310 .find(|t| t.domains.iter().any(|d| d == &domain))
311 .cloned()
312 })
313 .ok_or_else(|| not_found("DistributionTenant", &domain))?;
314 drop(state);
315 let body = render_distribution_tenant(&t);
316 Ok(xml_with_etag(StatusCode::OK, body, &t.etag, None))
317 }
318
319 pub(crate) fn update_distribution_tenant(
320 &self,
321 req: &AwsRequest,
322 route: &Route,
323 ) -> Result<AwsResponse, AwsServiceError> {
324 let id = route_id(route, "DistributionTenant")?;
325 let if_match = require_if_match(req)?;
326 let parsed: UpdateDistributionTenantRequest =
327 xml_io::from_xml_root(&req.body).map_err(|e| {
328 invalid_argument(format!("invalid UpdateDistributionTenantRequest XML: {e}"))
329 })?;
330 let mut state = self.state.write();
331 let account = state
332 .accounts
333 .get_mut(DEFAULT_ACCOUNT)
334 .ok_or_else(|| not_found("DistributionTenant", &id))?;
335 let t = account
336 .distribution_tenants
337 .get_mut(&id)
338 .ok_or_else(|| not_found("DistributionTenant", &id))?;
339 if t.etag != if_match {
340 return Err(precondition_failed());
341 }
342 if let Some(d) = parsed.distribution_id {
343 t.distribution_id = d;
344 }
345 if let Some(d) = parsed.domains {
346 t.domains = d.members.into_iter().map(|i| i.domain).collect();
347 }
348 if let Some(c) = parsed.connection_group_id {
349 t.connection_group_id = Some(c);
350 }
351 if let Some(e) = parsed.enabled {
352 t.enabled = e;
353 }
354 if let Some(p) = parsed.parameters {
355 t.parameters = convert_parameters(Some(p));
356 }
357 if let Some(c) = parsed.customizations {
358 let converted = convert_customizations(c);
359 t.web_acl_arn = converted
360 .web_acl
361 .as_ref()
362 .and_then(|w| w.arn.clone())
363 .or_else(|| t.web_acl_arn.clone());
364 t.customizations = Some(converted);
365 }
366 t.etag = generate_id_with_prefix("E");
367 t.last_modified_time = Utc::now();
368 t.status = "InProgress".to_string();
371 let snap = t.clone();
372 drop(state);
373 self.schedule_distribution_tenant_deploy(id.clone());
374 let body = render_distribution_tenant(&snap);
375 Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
376 }
377
378 pub(crate) fn delete_distribution_tenant(
379 &self,
380 req: &AwsRequest,
381 route: &Route,
382 ) -> Result<AwsResponse, AwsServiceError> {
383 let id = route_id(route, "DistributionTenant")?;
384 let if_match = require_if_match(req)?;
385 let mut state = self.state.write();
386 let account = state
387 .accounts
388 .get_mut(DEFAULT_ACCOUNT)
389 .ok_or_else(|| not_found("DistributionTenant", &id))?;
390 let t = account
391 .distribution_tenants
392 .get(&id)
393 .ok_or_else(|| not_found("DistributionTenant", &id))?;
394 if t.etag != if_match {
395 return Err(precondition_failed());
396 }
397 if t.enabled {
398 return Err(aws_error(
399 StatusCode::PRECONDITION_FAILED,
400 "ResourceInUse",
401 "DistributionTenant must be disabled before delete",
402 ));
403 }
404 let arn = t.arn.clone();
405 account.distribution_tenants.remove(&id);
406 account
407 .tenant_invalidations
408 .retain(|_, inv| inv.tenant_id != id);
409 account.tags.remove(&arn);
410 drop(state);
411 Ok(crate::policies::empty(StatusCode::NO_CONTENT))
412 }
413
414 pub(crate) fn list_distribution_tenants(
415 &self,
416 _req: &AwsRequest,
417 ) -> Result<AwsResponse, AwsServiceError> {
418 let state = self.state.read();
419 let mut items: Vec<StoredDistributionTenant> = state
420 .accounts
421 .get(DEFAULT_ACCOUNT)
422 .map(|a| a.distribution_tenants.values().cloned().collect())
423 .unwrap_or_default();
424 drop(state);
425 items.sort_by(|a, b| a.id.cmp(&b.id));
426 let body = render_tenant_list(&items, "ListDistributionTenantsResult");
427 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
428 }
429
430 pub(crate) fn list_distribution_tenants_by_customization(
431 &self,
432 _req: &AwsRequest,
433 ) -> Result<AwsResponse, AwsServiceError> {
434 let state = self.state.read();
435 let mut items: Vec<StoredDistributionTenant> = state
436 .accounts
437 .get(DEFAULT_ACCOUNT)
438 .map(|a| a.distribution_tenants.values().cloned().collect())
439 .unwrap_or_default();
440 drop(state);
441 items.sort_by(|a, b| a.id.cmp(&b.id));
442 let body = render_tenant_list(&items, "ListDistributionTenantsByCustomizationResult");
443 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
444 }
445
446 pub(crate) fn associate_distribution_tenant_web_acl(
447 &self,
448 req: &AwsRequest,
449 route: &Route,
450 ) -> Result<AwsResponse, AwsServiceError> {
451 let id = route_id(route, "DistributionTenant")?;
452 let if_match = require_if_match(req)?;
453 let parsed: AssociateWebAclRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
454 invalid_argument(format!(
455 "invalid AssociateDistributionTenantWebACLRequest XML: {e}"
456 ))
457 })?;
458 let mut state = self.state.write();
459 let account = state
460 .accounts
461 .get_mut(DEFAULT_ACCOUNT)
462 .ok_or_else(|| not_found("DistributionTenant", &id))?;
463 let t = account
464 .distribution_tenants
465 .get_mut(&id)
466 .ok_or_else(|| not_found("DistributionTenant", &id))?;
467 if t.etag != if_match {
468 return Err(precondition_failed());
469 }
470 t.web_acl_arn = Some(parsed.web_acl_arn.clone());
471 let customizations = t.customizations.get_or_insert_with(Default::default);
474 let web_acl = customizations.web_acl.get_or_insert_with(Default::default);
475 if web_acl.action.is_empty() {
476 web_acl.action = "override".to_string();
477 }
478 web_acl.arn = Some(parsed.web_acl_arn);
479 t.etag = generate_id_with_prefix("E");
480 t.last_modified_time = Utc::now();
481 let snap = t.clone();
482 drop(state);
483 let body = render_associate_web_acl(&snap);
484 Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
485 }
486
487 pub(crate) fn disassociate_distribution_tenant_web_acl(
488 &self,
489 req: &AwsRequest,
490 route: &Route,
491 ) -> Result<AwsResponse, AwsServiceError> {
492 let id = route_id(route, "DistributionTenant")?;
493 let if_match = require_if_match(req)?;
494 let mut state = self.state.write();
495 let account = state
496 .accounts
497 .get_mut(DEFAULT_ACCOUNT)
498 .ok_or_else(|| not_found("DistributionTenant", &id))?;
499 let t = account
500 .distribution_tenants
501 .get_mut(&id)
502 .ok_or_else(|| not_found("DistributionTenant", &id))?;
503 if t.etag != if_match {
504 return Err(precondition_failed());
505 }
506 t.web_acl_arn = None;
507 if let Some(c) = t.customizations.as_mut() {
508 c.web_acl = None;
509 }
510 t.etag = generate_id_with_prefix("E");
511 t.last_modified_time = Utc::now();
512 let snap = t.clone();
513 drop(state);
514 let body = render_disassociate_web_acl(&snap);
515 Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
516 }
517
518 pub(crate) fn create_invalidation_for_distribution_tenant(
519 &self,
520 req: &AwsRequest,
521 route: &Route,
522 ) -> Result<AwsResponse, AwsServiceError> {
523 let tenant_id = route_id(route, "DistributionTenant")?;
524 let parsed: InvalidationBatchRequest = xml_io::from_xml_root(&req.body)
525 .map_err(|e| invalid_argument(format!("invalid InvalidationBatch XML: {e}")))?;
526 if parsed.caller_reference.is_empty() {
527 return Err(invalid_argument("CallerReference is required"));
528 }
529 let paths = parsed.paths.items.map(|i| i.path).unwrap_or_default();
530 if paths.is_empty() {
531 return Err(invalid_argument(
532 "InvalidationBatch.Paths must be non-empty",
533 ));
534 }
535 let mut state = self.state.write();
536 let account = state.entry(DEFAULT_ACCOUNT);
537 if !account.distribution_tenants.contains_key(&tenant_id) {
538 return Err(not_found("DistributionTenant", &tenant_id));
539 }
540 let id = generate_invalidation_id();
541 let stored = StoredTenantInvalidation {
542 id: id.clone(),
543 tenant_id: tenant_id.clone(),
544 status: "Completed".to_string(),
545 create_time: Utc::now(),
546 paths,
547 caller_reference: parsed.caller_reference,
548 };
549 account
550 .tenant_invalidations
551 .insert(id.clone(), stored.clone());
552 drop(state);
553 let body = render_tenant_invalidation(&stored);
554 let mut headers = HeaderMap::new();
555 if let Ok(v) = http::HeaderValue::from_str(&format!(
556 "/2020-05-31/distribution-tenant/{tenant_id}/invalidation/{}",
557 stored.id
558 )) {
559 headers.insert(LOCATION, v);
560 }
561 Ok(xml_response(StatusCode::CREATED, body, headers))
562 }
563
564 pub(crate) fn get_invalidation_for_distribution_tenant(
565 &self,
566 route: &Route,
567 ) -> Result<AwsResponse, AwsServiceError> {
568 let tenant_id = route
569 .id
570 .as_deref()
571 .ok_or_else(|| invalid_argument("missing distribution tenant id"))?;
572 let inv_id = route
573 .second_id
574 .as_deref()
575 .ok_or_else(|| invalid_argument("missing invalidation id"))?;
576 let state = self.state.read();
577 let account = state
578 .accounts
579 .get(DEFAULT_ACCOUNT)
580 .ok_or_else(|| not_found("Invalidation", inv_id))?;
581 if !account.distribution_tenants.contains_key(tenant_id) {
582 return Err(not_found("DistributionTenant", tenant_id));
583 }
584 let inv = account
585 .tenant_invalidations
586 .get(inv_id)
587 .filter(|i| i.tenant_id == tenant_id)
588 .ok_or_else(|| not_found("Invalidation", inv_id))?
589 .clone();
590 drop(state);
591 let body = render_tenant_invalidation(&inv);
592 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
593 }
594
595 pub(crate) fn list_invalidations_for_distribution_tenant(
596 &self,
597 route: &Route,
598 ) -> Result<AwsResponse, AwsServiceError> {
599 let tenant_id = route
600 .id
601 .as_deref()
602 .ok_or_else(|| invalid_argument("missing distribution tenant id"))?;
603 let state = self.state.read();
604 let account = state
605 .accounts
606 .get(DEFAULT_ACCOUNT)
607 .ok_or_else(|| not_found("DistributionTenant", tenant_id))?;
608 if !account.distribution_tenants.contains_key(tenant_id) {
609 return Err(not_found("DistributionTenant", tenant_id));
610 }
611 let mut items: Vec<&StoredTenantInvalidation> = account
612 .tenant_invalidations
613 .values()
614 .filter(|i| i.tenant_id == tenant_id)
615 .collect();
616 items.sort_by_key(|a| a.create_time);
617 let body = render_tenant_invalidation_list(&items);
618 drop(state);
619 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
620 }
621}
622
623fn generate_tenant_id() -> String {
624 let raw = Uuid::new_v4().simple().to_string().to_uppercase();
625 format!("DT{}", &raw[..12])
626}
627
628fn generate_invalidation_id() -> String {
629 let raw = Uuid::new_v4().simple().to_string().to_uppercase();
630 format!("I{}", &raw[..13])
631}
632
633fn render_distribution_tenant(t: &StoredDistributionTenant) -> String {
634 let mut out = String::with_capacity(512);
635 out.push_str(XML_DECL);
636 out.push_str(&format!("<DistributionTenant xmlns=\"{NS}\">"));
637 push_tenant_inner(&mut out, t);
638 out.push_str("</DistributionTenant>");
639 out
640}
641
642fn push_tenant_inner(out: &mut String, t: &StoredDistributionTenant) {
643 out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
644 out.push_str(&format!(
645 "<DistributionId>{}</DistributionId>",
646 esc(&t.distribution_id)
647 ));
648 out.push_str(&format!("<Name>{}</Name>", esc(&t.name)));
649 out.push_str(&format!("<Arn>{}</Arn>", esc(&t.arn)));
650 out.push_str(&format!("<Status>{}</Status>", esc(&t.status)));
651 out.push_str(&format!("<Enabled>{}</Enabled>", t.enabled));
652 out.push_str(&format!(
653 "<CreatedTime>{}</CreatedTime>",
654 rfc3339(&t.created_time)
655 ));
656 out.push_str(&format!(
657 "<LastModifiedTime>{}</LastModifiedTime>",
658 rfc3339(&t.last_modified_time)
659 ));
660 if let Some(c) = &t.connection_group_id {
661 out.push_str(&format!(
662 "<ConnectionGroupId>{}</ConnectionGroupId>",
663 esc(c)
664 ));
665 }
666 out.push_str("<Domains>");
667 for d in &t.domains {
668 out.push_str("<DomainResult>");
669 out.push_str(&format!("<Domain>{}</Domain>", esc(d)));
670 out.push_str("<Status>active</Status>");
671 out.push_str("</DomainResult>");
672 }
673 out.push_str("</Domains>");
674 if !t.parameters.is_empty() {
675 out.push_str("<Parameters>");
676 for p in &t.parameters {
677 out.push_str("<Parameter>");
678 out.push_str(&format!("<Name>{}</Name>", esc(&p.name)));
679 out.push_str(&format!("<Value>{}</Value>", esc(&p.value)));
680 out.push_str("</Parameter>");
681 }
682 out.push_str("</Parameters>");
683 }
684 if let Some(c) = &t.customizations {
685 out.push_str("<Customizations>");
686 if let Some(w) = &c.web_acl {
687 out.push_str("<WebAcl>");
688 out.push_str(&format!("<Action>{}</Action>", esc(&w.action)));
689 if let Some(arn) = &w.arn {
690 out.push_str(&format!("<Arn>{}</Arn>", esc(arn)));
691 }
692 out.push_str("</WebAcl>");
693 }
694 if let Some(cert) = &c.certificate {
695 out.push_str("<Certificate>");
696 out.push_str(&format!("<Arn>{}</Arn>", esc(cert)));
697 out.push_str("</Certificate>");
698 }
699 if let Some(g) = &c.geo_restrictions {
700 out.push_str("<GeoRestrictions>");
701 out.push_str(&format!(
702 "<RestrictionType>{}</RestrictionType>",
703 esc(&g.restriction_type)
704 ));
705 out.push_str("<Locations>");
706 for l in &g.locations {
707 out.push_str(&format!("<Location>{}</Location>", esc(l)));
708 }
709 out.push_str("</Locations>");
710 out.push_str("</GeoRestrictions>");
711 }
712 out.push_str("</Customizations>");
713 }
714}
715
716fn convert_parameters(req: Option<ParametersReq>) -> Vec<TenantParameter> {
717 req.map(|p| {
718 p.parameter
719 .into_iter()
720 .map(|r| TenantParameter {
721 name: r.name,
722 value: r.value,
723 })
724 .collect()
725 })
726 .unwrap_or_default()
727}
728
729fn convert_customizations(req: CustomizationsReq) -> TenantCustomizations {
730 TenantCustomizations {
731 web_acl: req.web_acl.map(|w| TenantWebAclCustomization {
732 action: w.action,
733 arn: w.arn,
734 }),
735 certificate: req.certificate.map(|c| c.arn),
736 geo_restrictions: req
737 .geo_restrictions
738 .map(|g| TenantGeoRestrictionCustomization {
739 restriction_type: g.restriction_type,
740 locations: g.locations.map(|l| l.location).unwrap_or_default(),
741 }),
742 }
743}
744
745fn render_tenant_list(items: &[StoredDistributionTenant], wrapper: &str) -> String {
746 let mut out = String::with_capacity(512);
747 out.push_str(XML_DECL);
748 out.push_str(&format!("<{wrapper} xmlns=\"{NS}\">"));
749 out.push_str("<NextMarker></NextMarker>");
750 out.push_str("<DistributionTenantList>");
751 for t in items {
752 out.push_str("<DistributionTenantSummary>");
753 push_tenant_inner(&mut out, t);
754 out.push_str("</DistributionTenantSummary>");
755 }
756 out.push_str("</DistributionTenantList>");
757 out.push_str(&format!("</{wrapper}>"));
758 out
759}
760
761fn render_associate_web_acl(t: &StoredDistributionTenant) -> String {
762 let mut out = String::with_capacity(256);
763 out.push_str(XML_DECL);
764 out.push_str(&format!(
765 "<AssociateDistributionTenantWebACLResult xmlns=\"{NS}\">"
766 ));
767 out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
768 if let Some(arn) = &t.web_acl_arn {
769 out.push_str(&format!("<WebACLArn>{}</WebACLArn>", esc(arn)));
770 }
771 out.push_str("</AssociateDistributionTenantWebACLResult>");
772 out
773}
774
775fn render_disassociate_web_acl(t: &StoredDistributionTenant) -> String {
776 let mut out = String::with_capacity(256);
777 out.push_str(XML_DECL);
778 out.push_str(&format!(
779 "<DisassociateDistributionTenantWebACLResult xmlns=\"{NS}\">"
780 ));
781 out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
782 out.push_str("</DisassociateDistributionTenantWebACLResult>");
783 out
784}
785
786fn render_tenant_invalidation(inv: &StoredTenantInvalidation) -> String {
787 let mut out = String::with_capacity(512);
788 out.push_str(XML_DECL);
789 out.push_str(&format!("<Invalidation xmlns=\"{NS}\">"));
790 out.push_str(&format!("<Id>{}</Id>", esc(&inv.id)));
791 out.push_str(&format!("<Status>{}</Status>", esc(&inv.status)));
792 out.push_str(&format!(
793 "<CreateTime>{}</CreateTime>",
794 rfc3339(&inv.create_time)
795 ));
796 out.push_str("<InvalidationBatch>");
797 out.push_str(&format!(
798 "<CallerReference>{}</CallerReference>",
799 esc(&inv.caller_reference)
800 ));
801 out.push_str("<Paths>");
802 out.push_str(&format!("<Quantity>{}</Quantity>", inv.paths.len()));
803 out.push_str("<Items>");
804 for p in &inv.paths {
805 out.push_str(&format!("<Path>{}</Path>", esc(p)));
806 }
807 out.push_str("</Items>");
808 out.push_str("</Paths>");
809 out.push_str("</InvalidationBatch>");
810 out.push_str("</Invalidation>");
811 out
812}
813
814fn render_tenant_invalidation_list(items: &[&StoredTenantInvalidation]) -> String {
815 let mut out = String::with_capacity(1024);
816 out.push_str(XML_DECL);
817 out.push_str(&format!("<InvalidationList xmlns=\"{NS}\">"));
818 out.push_str("<Marker></Marker>");
819 out.push_str("<MaxItems>100</MaxItems>");
820 out.push_str("<IsTruncated>false</IsTruncated>");
821 out.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
822 if !items.is_empty() {
823 out.push_str("<Items>");
824 for inv in items {
825 out.push_str("<InvalidationSummary>");
826 out.push_str(&format!("<Id>{}</Id>", esc(&inv.id)));
827 out.push_str(&format!(
828 "<CreateTime>{}</CreateTime>",
829 rfc3339(&inv.create_time)
830 ));
831 out.push_str(&format!("<Status>{}</Status>", esc(&inv.status)));
832 out.push_str("</InvalidationSummary>");
833 }
834 out.push_str("</Items>");
835 }
836 out.push_str("</InvalidationList>");
837 out
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use crate::state::CloudFrontAccounts;
844 use bytes::Bytes;
845 use fakecloud_core::service::AwsService;
846 use http::HeaderValue;
847 use parking_lot::RwLock;
848 use std::sync::Arc;
849
850 fn svc() -> CloudFrontService {
851 CloudFrontService::new(Arc::new(RwLock::new(CloudFrontAccounts::new())))
852 }
853
854 fn req(method: http::Method, path: &str, body: &str, if_match: Option<&str>) -> AwsRequest {
855 let mut headers = HeaderMap::new();
856 if let Some(v) = if_match {
857 headers.insert(http::header::IF_MATCH, HeaderValue::from_str(v).unwrap());
858 }
859 AwsRequest {
860 service: "cloudfront".into(),
861 action: String::new(),
862 region: "us-east-1".into(),
863 account_id: DEFAULT_ACCOUNT.into(),
864 request_id: Uuid::new_v4().to_string(),
865 headers,
866 query_params: std::collections::HashMap::new(),
867 body_stream: parking_lot::Mutex::new(None),
868 body: Bytes::from(body.to_string()),
869 path_segments: path
870 .split('/')
871 .filter(|s| !s.is_empty())
872 .map(String::from)
873 .collect(),
874 raw_path: path.into(),
875 raw_query: String::new(),
876 method,
877 is_query_protocol: false,
878 access_key_id: None,
879 principal: None,
880 }
881 }
882
883 fn body_str(resp: &AwsResponse) -> String {
884 match &resp.body {
885 fakecloud_core::service::ResponseBody::Bytes(b) => {
886 String::from_utf8(b.to_vec()).unwrap()
887 }
888 _ => panic!("expected bytes body"),
889 }
890 }
891
892 async fn create_tenant_full(svc: &CloudFrontService, name: &str) -> (String, String) {
893 let body = format!(
894 r#"<?xml version="1.0"?>
895<CreateDistributionTenantRequest xmlns="{NS}">
896 <DistributionId>E123</DistributionId>
897 <Name>{name}</Name>
898 <Parameters>
899 <Parameter><Name>tenantId</Name><Value>acme</Value></Parameter>
900 </Parameters>
901 <Customizations>
902 <Certificate><Arn>arn:aws:acm::cert/xyz</Arn></Certificate>
903 <GeoRestrictions>
904 <RestrictionType>whitelist</RestrictionType>
905 <Locations><Location>US</Location><Location>CA</Location></Locations>
906 </GeoRestrictions>
907 </Customizations>
908 <ManagedCertificateRequest>
909 <PrimaryDomainName>acme.example.com</PrimaryDomainName>
910 <ValidationTokenHost>self-hosted</ValidationTokenHost>
911 </ManagedCertificateRequest>
912 <Tags>
913 <Items><Tag><Key>env</Key><Value>prod</Value></Tag></Items>
914 </Tags>
915</CreateDistributionTenantRequest>"#
916 );
917 let resp = svc
918 .handle(req(
919 http::Method::POST,
920 "/2020-05-31/distribution-tenant",
921 &body,
922 None,
923 ))
924 .await
925 .unwrap();
926 assert_eq!(resp.status, StatusCode::CREATED);
927 let xml = body_str(&resp);
928 let id = xml
929 .split("<Id>")
930 .nth(1)
931 .unwrap()
932 .split("</Id>")
933 .next()
934 .unwrap()
935 .to_string();
936 let etag = resp
937 .headers
938 .get(http::header::ETAG)
939 .unwrap()
940 .to_str()
941 .unwrap()
942 .to_string();
943 (id, etag)
944 }
945
946 #[tokio::test]
947 async fn create_tenant_persists_parameters_customizations_and_tags() {
948 let svc = svc();
951 let (id, _etag) = create_tenant_full(&svc, "acme-tenant").await;
952
953 let get = svc
954 .handle(req(
955 http::Method::GET,
956 &format!("/2020-05-31/distribution-tenant/{id}"),
957 "",
958 None,
959 ))
960 .await
961 .unwrap();
962 let xml = body_str(&get);
963 assert!(
964 xml.contains("<Name>tenantId</Name>") && xml.contains("<Value>acme</Value>"),
965 "parameters dropped: {xml}"
966 );
967 assert!(
968 xml.contains("<Certificate><Arn>arn:aws:acm::cert/xyz</Arn></Certificate>"),
969 "certificate customization dropped: {xml}"
970 );
971 assert!(
972 xml.contains("<RestrictionType>whitelist</RestrictionType>")
973 && xml.contains("<Location>US</Location>"),
974 "geo restrictions dropped: {xml}"
975 );
976
977 let arn = format!(
979 "arn:aws:cloudfront::{}:distribution-tenant/{}",
980 DEFAULT_ACCOUNT, id
981 );
982 let state = svc.shared_state();
983 let guard = state.read();
984 let tags = guard
985 .get(DEFAULT_ACCOUNT)
986 .and_then(|a| a.tags.get(&arn))
987 .cloned()
988 .unwrap_or_default();
989 assert!(
990 tags.iter()
991 .any(|t| t.key == "env" && t.value.as_deref() == Some("prod")),
992 "tenant tags dropped: {tags:?}"
993 );
994 drop(guard);
995 assert!(
999 !xml.contains("ManagedCertificateRequest"),
1000 "ManagedCertificateRequest must not be echoed: {xml}"
1001 );
1002 }
1003
1004 #[tokio::test]
1005 async fn associate_web_acl_surfaces_on_get_distribution_tenant() {
1006 let svc = svc();
1009 let (id, etag) = create_tenant_full(&svc, "waf-tenant").await;
1010 let web_acl = "arn:aws:wafv2:us-east-1:000:global/webacl/x/1";
1011 let assoc_body = format!(
1012 r#"<?xml version="1.0"?>
1013<AssociateDistributionTenantWebACLRequest xmlns="{NS}">
1014 <WebACLArn>{web_acl}</WebACLArn>
1015</AssociateDistributionTenantWebACLRequest>"#
1016 );
1017 let assoc = svc
1018 .handle(req(
1019 http::Method::PUT,
1020 &format!("/2020-05-31/distribution-tenant/{id}/associate-web-acl"),
1021 &assoc_body,
1022 Some(&etag),
1023 ))
1024 .await
1025 .unwrap();
1026 assert_eq!(assoc.status, StatusCode::OK);
1027
1028 let get = svc
1029 .handle(req(
1030 http::Method::GET,
1031 &format!("/2020-05-31/distribution-tenant/{id}"),
1032 "",
1033 None,
1034 ))
1035 .await
1036 .unwrap();
1037 let xml = body_str(&get);
1038 assert!(
1039 xml.contains(&format!("<Arn>{web_acl}</Arn>")) && xml.contains("<WebAcl>"),
1040 "WebACL not surfaced on GetDistributionTenant: {xml}"
1041 );
1042 }
1043}