1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use base64::Engine;
8use chrono::Utc;
9use http::StatusCode;
10use parking_lot::RwLock;
11use serde_json::{json, Value};
12use uuid::Uuid;
13
14use fakecloud_aws::arn::Arn;
15use fakecloud_core::pagination::paginate;
16use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
17
18use crate::evaluator::RateLimiter;
19use crate::state::{
20 AccountState, ApiKey, IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, Wafv2Accounts,
21 WebAcl,
22};
23
24const SUPPORTED_ACTIONS: &[&str] = &[
25 "AssociateWebACL",
26 "CheckCapacity",
27 "CreateAPIKey",
28 "CreateIPSet",
29 "CreateRegexPatternSet",
30 "CreateRuleGroup",
31 "CreateWebACL",
32 "DeleteAPIKey",
33 "DeleteFirewallManagerRuleGroups",
34 "DeleteIPSet",
35 "DeleteLoggingConfiguration",
36 "DeletePermissionPolicy",
37 "DeleteRegexPatternSet",
38 "DeleteRuleGroup",
39 "DeleteWebACL",
40 "DescribeAllManagedProducts",
41 "DescribeManagedProductsByVendor",
42 "DescribeManagedRuleGroup",
43 "DisassociateWebACL",
44 "GenerateMobileSdkReleaseUrl",
45 "GetDecryptedAPIKey",
46 "GetIPSet",
47 "GetLoggingConfiguration",
48 "GetManagedRuleSet",
49 "GetMobileSdkRelease",
50 "GetPermissionPolicy",
51 "GetRateBasedStatementManagedKeys",
52 "GetRegexPatternSet",
53 "GetRuleGroup",
54 "GetSampledRequests",
55 "GetTopPathStatisticsByTraffic",
56 "GetWebACL",
57 "GetWebACLForResource",
58 "ListAPIKeys",
59 "ListAvailableManagedRuleGroups",
60 "ListAvailableManagedRuleGroupVersions",
61 "ListIPSets",
62 "ListLoggingConfigurations",
63 "ListManagedRuleSets",
64 "ListMobileSdkReleases",
65 "ListRegexPatternSets",
66 "ListResourcesForWebACL",
67 "ListRuleGroups",
68 "ListTagsForResource",
69 "ListWebACLs",
70 "PutLoggingConfiguration",
71 "PutManagedRuleSetVersions",
72 "PutPermissionPolicy",
73 "TagResource",
74 "UntagResource",
75 "UpdateIPSet",
76 "UpdateManagedRuleSetVersionExpiryDate",
77 "UpdateRegexPatternSet",
78 "UpdateRuleGroup",
79 "UpdateWebACL",
80];
81
82pub struct Wafv2Service {
83 state: SharedWafv2State,
84 rate_limiter: Arc<RateLimiter>,
85}
86
87mod api_keys;
88mod capacity;
89mod ip_sets;
90mod logging;
91mod mobile_sdk;
92mod permission_policy;
93mod regex_pattern_sets;
94mod rule_groups;
95mod sampled_requests;
96mod tags;
97mod web_acls;
98
99impl Wafv2Service {
100 pub fn new(state: SharedWafv2State) -> Self {
101 Self::with_rate_limiter(state, Arc::new(RateLimiter::new()))
102 }
103
104 pub fn with_rate_limiter(state: SharedWafv2State, rate_limiter: Arc<RateLimiter>) -> Self {
108 Self {
109 state,
110 rate_limiter,
111 }
112 }
113
114 pub fn shared_state(&self) -> SharedWafv2State {
115 Arc::clone(&self.state)
116 }
117
118 pub fn rate_limiter(&self) -> Arc<RateLimiter> {
123 Arc::clone(&self.rate_limiter)
124 }
125}
126
127impl Default for Wafv2Service {
128 fn default() -> Self {
129 Self::new(Arc::new(RwLock::new(Wafv2Accounts::new())))
130 }
131}
132
133#[async_trait]
134impl AwsService for Wafv2Service {
135 fn service_name(&self) -> &str {
136 "wafv2"
137 }
138
139 fn supported_actions(&self) -> &[&str] {
140 SUPPORTED_ACTIONS
141 }
142
143 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
144 match req.action.as_str() {
145 "CreateWebACL" => self.create_web_acl(&req),
146 "GetWebACL" => self.get_web_acl(&req),
147 "ListWebACLs" => self.list_web_acls(&req),
148 "UpdateWebACL" => self.update_web_acl(&req),
149 "DeleteWebACL" => self.delete_web_acl(&req),
150 "CreateRuleGroup" => self.create_rule_group(&req),
151 "GetRuleGroup" => self.get_rule_group(&req),
152 "ListRuleGroups" => self.list_rule_groups(&req),
153 "UpdateRuleGroup" => self.update_rule_group(&req),
154 "DeleteRuleGroup" => self.delete_rule_group(&req),
155 "CreateIPSet" => self.create_ip_set(&req),
156 "GetIPSet" => self.get_ip_set(&req),
157 "ListIPSets" => self.list_ip_sets(&req),
158 "UpdateIPSet" => self.update_ip_set(&req),
159 "DeleteIPSet" => self.delete_ip_set(&req),
160 "CreateRegexPatternSet" => self.create_regex_pattern_set(&req),
161 "GetRegexPatternSet" => self.get_regex_pattern_set(&req),
162 "ListRegexPatternSets" => self.list_regex_pattern_sets(&req),
163 "UpdateRegexPatternSet" => self.update_regex_pattern_set(&req),
164 "DeleteRegexPatternSet" => self.delete_regex_pattern_set(&req),
165 "AssociateWebACL" => self.associate_web_acl(&req),
166 "DisassociateWebACL" => self.disassociate_web_acl(&req),
167 "GetWebACLForResource" => self.get_web_acl_for_resource(&req),
168 "ListResourcesForWebACL" => self.list_resources_for_web_acl(&req),
169 "PutLoggingConfiguration" => self.put_logging_configuration(&req),
170 "GetLoggingConfiguration" => self.get_logging_configuration(&req),
171 "DeleteLoggingConfiguration" => self.delete_logging_configuration(&req),
172 "ListLoggingConfigurations" => self.list_logging_configurations(&req),
173 "PutPermissionPolicy" => self.put_permission_policy(&req),
174 "GetPermissionPolicy" => self.get_permission_policy(&req),
175 "DeletePermissionPolicy" => self.delete_permission_policy(&req),
176 "TagResource" => self.tag_resource(&req),
177 "UntagResource" => self.untag_resource(&req),
178 "ListTagsForResource" => self.list_tags_for_resource(&req),
179 "CreateAPIKey" => self.create_api_key(&req),
180 "DeleteAPIKey" => self.delete_api_key(&req),
181 "GetDecryptedAPIKey" => self.get_decrypted_api_key(&req),
182 "ListAPIKeys" => self.list_api_keys(&req),
183 "DescribeAllManagedProducts" => self.describe_all_managed_products(&req),
184 "DescribeManagedProductsByVendor" => self.describe_managed_products_by_vendor(&req),
185 "DescribeManagedRuleGroup" => self.describe_managed_rule_group(&req),
186 "GetManagedRuleSet" => self.get_managed_rule_set(&req),
187 "ListAvailableManagedRuleGroups" => self.list_available_managed_rule_groups(&req),
188 "ListAvailableManagedRuleGroupVersions" => {
189 self.list_available_managed_rule_group_versions(&req)
190 }
191 "ListManagedRuleSets" => self.list_managed_rule_sets(&req),
192 "PutManagedRuleSetVersions" => self.put_managed_rule_set_versions(&req),
193 "UpdateManagedRuleSetVersionExpiryDate" => {
194 self.update_managed_rule_set_version_expiry_date(&req)
195 }
196 "GenerateMobileSdkReleaseUrl" => self.generate_mobile_sdk_release_url(&req),
197 "GetMobileSdkRelease" => self.get_mobile_sdk_release(&req),
198 "ListMobileSdkReleases" => self.list_mobile_sdk_releases(&req),
199 "CheckCapacity" => self.check_capacity(&req),
200 "GetSampledRequests" => self.get_sampled_requests(&req),
201 "GetTopPathStatisticsByTraffic" => self.get_top_path_statistics_by_traffic(&req),
202 "GetRateBasedStatementManagedKeys" => self.get_rate_based_statement_managed_keys(&req),
203 "DeleteFirewallManagerRuleGroups" => self.delete_firewall_manager_rule_groups(&req),
204 other => Err(AwsServiceError::action_not_implemented("wafv2", other)),
205 }
206 }
207}
208
209impl Wafv2Service {}
212
213impl Wafv2Service {}
216
217impl Wafv2Service {}
220
221impl Wafv2Service {}
224
225impl Wafv2Service {}
228
229impl Wafv2Service {}
232
233impl Wafv2Service {}
236
237impl Wafv2Service {}
240
241impl Wafv2Service {}
244
245impl Wafv2Service {}
248
249impl Wafv2Service {}
252
253impl Wafv2Service {
256 fn get_top_path_statistics_by_traffic(
257 &self,
258 req: &AwsRequest,
259 ) -> Result<AwsResponse, AwsServiceError> {
260 let body = req.json_body();
261 let _web_acl_arn = require_str_len(&body, "WebAclArn", 20, 2048)?;
264 let _scope = require_scope(&body)?;
265 body.get("TimeWindow")
266 .ok_or_else(|| invalid_param("TimeWindow is required"))?;
267 let _limit = require_int_range(&body, "Limit", 1, 100)?;
268 let _bots_per_path = require_int_range(&body, "NumberOfTopTrafficBotsPerPath", 1, 10)?;
269 opt_str_len(&body, "UriPathPrefix", 1, 512)?;
270 opt_str_len(&body, "BotCategory", 1, 256)?;
271 opt_str_len(&body, "BotName", 1, 256)?;
272 opt_str_len(&body, "BotOrganization", 1, 256)?;
273 validate_opt_next_marker(&body)?;
274 Ok(AwsResponse::ok_json(json!({
275 "PathStatistics": [],
276 "TopCategories": [],
277 "TotalRequestCount": 0_u64,
278 })))
279 }
280}
281
282fn account_mut<'a>(state: &'a mut Wafv2Accounts, account_id: &str) -> &'a mut AccountState {
285 state.accounts.entry(account_id.to_string()).or_default()
286}
287
288fn require_str(body: &Value, field: &str) -> Result<String, AwsServiceError> {
289 body.get(field)
290 .and_then(Value::as_str)
291 .map(str::to_owned)
292 .ok_or_else(|| invalid_param(format!("{field} is required")))
293}
294
295fn require_scope(body: &Value) -> Result<String, AwsServiceError> {
296 let scope = require_str(body, "Scope")?;
297 validate_enum(&scope, &["REGIONAL", "CLOUDFRONT"], "Scope")?;
298 Ok(scope)
299}
300
301fn validate_str_len(
303 value: &str,
304 min: usize,
305 max: usize,
306 field: &str,
307) -> Result<(), AwsServiceError> {
308 if value.len() < min || value.len() > max {
309 return Err(invalid_param(format!(
310 "{field} must be between {min} and {max} characters"
311 )));
312 }
313 Ok(())
314}
315
316fn validate_int_range(value: i64, min: i64, max: i64, field: &str) -> Result<(), AwsServiceError> {
318 if value < min || value > max {
319 return Err(invalid_param(format!(
320 "{field} must be between {min} and {max}"
321 )));
322 }
323 Ok(())
324}
325
326fn validate_enum(value: &str, allowed: &[&str], field: &str) -> Result<(), AwsServiceError> {
328 if !allowed.contains(&value) {
329 return Err(invalid_param(format!("Invalid {field}: {value}")));
330 }
331 Ok(())
332}
333
334fn opt_str_len(
337 body: &Value,
338 field: &str,
339 min: usize,
340 max: usize,
341) -> Result<Option<String>, AwsServiceError> {
342 match body.get(field).and_then(Value::as_str) {
343 Some(s) => {
344 validate_str_len(s, min, max, field)?;
345 Ok(Some(s.to_owned()))
346 }
347 None => Ok(None),
348 }
349}
350
351fn require_str_len(
353 body: &Value,
354 field: &str,
355 min: usize,
356 max: usize,
357) -> Result<String, AwsServiceError> {
358 let s = require_str(body, field)?;
359 validate_str_len(&s, min, max, field)?;
360 Ok(s)
361}
362
363fn opt_int_range(
365 body: &Value,
366 field: &str,
367 min: i64,
368 max: i64,
369) -> Result<Option<i64>, AwsServiceError> {
370 match body.get(field) {
371 Some(v) => {
372 let n = v
373 .as_i64()
374 .ok_or_else(|| invalid_param(format!("{field} must be an integer")))?;
375 validate_int_range(n, min, max, field)?;
376 Ok(Some(n))
377 }
378 None => Ok(None),
379 }
380}
381
382fn require_int_range(
384 body: &Value,
385 field: &str,
386 min: i64,
387 max: i64,
388) -> Result<i64, AwsServiceError> {
389 let v = body
390 .get(field)
391 .ok_or_else(|| invalid_param(format!("{field} is required")))?;
392 let n = v
393 .as_i64()
394 .ok_or_else(|| invalid_param(format!("{field} must be an integer")))?;
395 validate_int_range(n, min, max, field)?;
396 Ok(n)
397}
398
399fn validate_opt_limit(body: &Value) -> Result<(), AwsServiceError> {
402 opt_int_range(body, "Limit", 1, 100)?;
403 Ok(())
404}
405
406fn validate_opt_next_marker(body: &Value) -> Result<(), AwsServiceError> {
409 opt_str_len(body, "NextMarker", 1, 256)?;
410 Ok(())
411}
412
413fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
414 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "WAFInvalidParameterException", msg)
415}
416
417fn normalize_resource_arn(arn: &str) -> String {
428 if let Some(rest) = arn.strip_prefix("arn:aws:elasticloadbalancing:") {
433 if let Some((before, after)) = rest.split_once(":listener/") {
434 let mut parts = after.splitn(4, '/');
437 let ty = parts.next();
438 let name = parts.next();
439 let lb_suffix = parts.next();
440 if let (Some(ty), Some(name), Some(lb_suffix)) = (ty, name, lb_suffix) {
441 return format!(
442 "arn:aws:elasticloadbalancing:{before}:loadbalancer/{ty}/{name}/{lb_suffix}"
443 );
444 }
445 }
446 }
447 arn.to_string()
448}
449
450fn not_found(resource: &str) -> AwsServiceError {
451 AwsServiceError::aws_error(
452 StatusCode::BAD_REQUEST,
453 "WAFNonexistentItemException",
454 format!("{resource} not found"),
455 )
456}
457
458fn already_exists(msg: &str) -> AwsServiceError {
459 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "WAFDuplicateItemException", msg)
460}
461
462fn stale_lock_token() -> AwsServiceError {
463 AwsServiceError::aws_error(
464 StatusCode::BAD_REQUEST,
465 "WAFOptimisticLockException",
466 "LockToken does not match the current value; refresh and retry",
467 )
468}
469
470fn synth_uuid() -> String {
471 Uuid::new_v4().to_string()
472}
473
474fn synth_arn(
475 account_id: &str,
476 region: &str,
477 scope: &str,
478 kind: &str,
479 name: &str,
480 id: &str,
481) -> String {
482 let region = if region.is_empty() {
483 "us-east-1"
484 } else {
485 region
486 };
487 let (region_in_arn, scope_seg) = if scope == "CLOUDFRONT" {
491 ("us-east-1", "global")
492 } else {
493 (region, region)
494 };
495 Arn::new(
496 "wafv2",
497 region_in_arn,
498 account_id,
499 &format!("{scope_seg}/{kind}/{name}/{id}"),
500 )
501 .to_string()
502}
503
504fn parse_string_list(value: Option<&Value>) -> Vec<String> {
505 value
506 .and_then(Value::as_array)
507 .map(|v| {
508 v.iter()
509 .filter_map(|s| s.as_str().map(str::to_owned))
510 .collect()
511 })
512 .unwrap_or_default()
513}
514
515fn parse_tags(value: Option<&Value>) -> Result<BTreeMap<String, String>, AwsServiceError> {
516 let mut out = BTreeMap::new();
517 let Some(arr) = value.and_then(Value::as_array) else {
518 return Ok(out);
519 };
520 for tag in arr {
521 let key = tag
522 .get("Key")
523 .and_then(Value::as_str)
524 .ok_or_else(|| invalid_param("Tag.Key is required"))?
525 .to_string();
526 let value = tag
527 .get("Value")
528 .and_then(Value::as_str)
529 .unwrap_or_default()
530 .to_string();
531 out.insert(key, value);
532 }
533 Ok(out)
534}
535
536fn parse_custom_response_bodies(value: Option<&Value>) -> BTreeMap<String, Value> {
537 value
538 .and_then(Value::as_object)
539 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
540 .unwrap_or_default()
541}
542
543fn resource_exists(account: &AccountState, arn: &str) -> bool {
544 account.web_acls.values().any(|w| w.arn == arn)
545 || account.rule_groups.values().any(|r| r.arn == arn)
546 || account.ip_sets.values().any(|s| s.arn == arn)
547 || account.regex_pattern_sets.values().any(|s| s.arn == arn)
548}
549
550fn compute_capacity(rules: &[Value]) -> i64 {
554 rules
555 .iter()
556 .map(|r| r.get("Statement").map(count_statement_leaves).unwrap_or(1) as i64)
557 .sum()
558}
559
560fn count_statement_leaves(stmt: &Value) -> u32 {
561 let Some(obj) = stmt.as_object() else {
562 return 1;
563 };
564 let mut total = 0u32;
565 for (k, v) in obj {
566 match k.as_str() {
567 "AndStatement" | "OrStatement" => {
568 if let Some(arr) = v.get("Statements").and_then(Value::as_array) {
569 for s in arr {
570 total += count_statement_leaves(s);
571 }
572 }
573 }
574 "NotStatement" => {
575 if let Some(s) = v.get("Statement") {
576 total += count_statement_leaves(s);
577 }
578 }
579 _ => {
580 total += 1;
581 }
582 }
583 }
584 total.max(1)
585}
586
587fn managed_products() -> Vec<Value> {
588 vec![
589 json!({
590 "VendorName": "AWS",
591 "ManagedRuleSetName": "AWSManagedRulesCommonRuleSet",
592 "ProductId": "prod-aws-common",
593 "ProductLink": "https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html",
594 "ProductTitle": "Core rule set",
595 "ProductDescription": "OWASP Top 10 baseline rules",
596 "SnsTopicArn": "arn:aws:sns:us-east-1::aws-managed-common-notifications",
597 "IsVersioningSupported": true,
598 "IsAdvancedManagedRuleSet": false,
599 }),
600 json!({
601 "VendorName": "AWS",
602 "ManagedRuleSetName": "AWSManagedRulesSQLiRuleSet",
603 "ProductId": "prod-aws-sqli",
604 "ProductLink": "https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html",
605 "ProductTitle": "SQL injection rule set",
606 "ProductDescription": "Rules that block SQL injection patterns",
607 "SnsTopicArn": "arn:aws:sns:us-east-1::aws-managed-sqli-notifications",
608 "IsVersioningSupported": true,
609 "IsAdvancedManagedRuleSet": false,
610 }),
611 ]
612}
613
614fn managed_rule_summaries(_vendor: &str, _name: &str) -> Vec<Value> {
615 vec![json!({
616 "Name": "RuleA",
617 "Action": {"Block": {}},
618 })]
619}
620
621fn web_acl_summary_json(
624 id: &str,
625 name: &str,
626 arn: &str,
627 description: Option<&str>,
628 lock_token: &str,
629) -> Value {
630 let mut obj = json!({
631 "Id": id,
632 "Name": name,
633 "ARN": arn,
634 "LockToken": lock_token,
635 });
636 if let Some(d) = description {
637 obj.as_object_mut()
638 .unwrap()
639 .insert("Description".to_string(), Value::String(d.to_string()));
640 }
641 obj
642}
643
644fn web_acl_detail_json(acl: &WebAcl) -> Value {
645 let mut obj = json!({
646 "Id": acl.id,
647 "Name": acl.name,
648 "ARN": acl.arn,
649 "DefaultAction": acl.default_action,
650 "Rules": acl.rules,
651 "VisibilityConfig": acl.visibility_config,
652 "Capacity": acl.capacity,
653 "ManagedByFirewallManager": acl.managed_by_firewall_manager,
654 "RetrofittedByFirewallManager": acl.retrofitted_by_firewall_manager,
655 "LabelNamespace": acl.label_namespace,
656 "TokenDomains": acl.token_domains,
657 "PreProcessFirewallManagerRuleGroups": acl.pre_process_firewall_manager_rule_groups,
658 "PostProcessFirewallManagerRuleGroups": acl.post_process_firewall_manager_rule_groups,
659 });
660 if let Some(d) = &acl.description {
661 obj.as_object_mut()
662 .unwrap()
663 .insert("Description".to_string(), Value::String(d.clone()));
664 }
665 if !acl.custom_response_bodies.is_empty() {
666 obj.as_object_mut().unwrap().insert(
667 "CustomResponseBodies".to_string(),
668 json!(acl.custom_response_bodies),
669 );
670 }
671 if let Some(c) = &acl.captcha_config {
672 obj.as_object_mut()
673 .unwrap()
674 .insert("CaptchaConfig".to_string(), c.clone());
675 }
676 if let Some(c) = &acl.challenge_config {
677 obj.as_object_mut()
678 .unwrap()
679 .insert("ChallengeConfig".to_string(), c.clone());
680 }
681 if let Some(c) = &acl.association_config {
682 obj.as_object_mut()
683 .unwrap()
684 .insert("AssociationConfig".to_string(), c.clone());
685 }
686 if let Some(c) = &acl.data_protection_config {
687 obj.as_object_mut()
688 .unwrap()
689 .insert("DataProtectionConfig".to_string(), c.clone());
690 }
691 if let Some(c) = &acl.on_source_d_do_s_protection_config {
692 obj.as_object_mut()
693 .unwrap()
694 .insert("OnSourceDDoSProtectionConfig".to_string(), c.clone());
695 }
696 if let Some(c) = &acl.application_config {
697 obj.as_object_mut()
698 .unwrap()
699 .insert("ApplicationConfig".to_string(), c.clone());
700 }
701 obj
702}
703
704fn rule_group_summary_json(
705 id: &str,
706 name: &str,
707 arn: &str,
708 description: Option<&str>,
709 lock_token: &str,
710) -> Value {
711 let mut obj = json!({
712 "Id": id,
713 "Name": name,
714 "ARN": arn,
715 "LockToken": lock_token,
716 });
717 if let Some(d) = description {
718 obj.as_object_mut()
719 .unwrap()
720 .insert("Description".to_string(), Value::String(d.to_string()));
721 }
722 obj
723}
724
725fn rule_group_detail_json(rg: &RuleGroup) -> Value {
726 let mut obj = json!({
727 "Id": rg.id,
728 "Name": rg.name,
729 "ARN": rg.arn,
730 "Capacity": rg.capacity,
731 "Rules": rg.rules,
732 "VisibilityConfig": rg.visibility_config,
733 "LabelNamespace": rg.label_namespace,
734 "AvailableLabels": rg.available_labels,
735 "ConsumedLabels": rg.consumed_labels,
736 });
737 if let Some(d) = &rg.description {
738 obj.as_object_mut()
739 .unwrap()
740 .insert("Description".to_string(), Value::String(d.clone()));
741 }
742 if !rg.custom_response_bodies.is_empty() {
743 obj.as_object_mut().unwrap().insert(
744 "CustomResponseBodies".to_string(),
745 json!(rg.custom_response_bodies),
746 );
747 }
748 obj
749}
750
751fn ip_set_summary_json(
752 id: &str,
753 name: &str,
754 arn: &str,
755 description: Option<&str>,
756 lock_token: &str,
757) -> Value {
758 let mut obj = json!({
759 "Id": id,
760 "Name": name,
761 "ARN": arn,
762 "LockToken": lock_token,
763 });
764 if let Some(d) = description {
765 obj.as_object_mut()
766 .unwrap()
767 .insert("Description".to_string(), Value::String(d.to_string()));
768 }
769 obj
770}
771
772fn ip_set_detail_json(set: &IpSet) -> Value {
773 let mut obj = json!({
774 "Id": set.id,
775 "Name": set.name,
776 "ARN": set.arn,
777 "IPAddressVersion": set.ip_address_version,
778 "Addresses": set.addresses,
779 });
780 if let Some(d) = &set.description {
781 obj.as_object_mut()
782 .unwrap()
783 .insert("Description".to_string(), Value::String(d.clone()));
784 }
785 obj
786}
787
788fn regex_set_summary_json(
789 id: &str,
790 name: &str,
791 arn: &str,
792 description: Option<&str>,
793 lock_token: &str,
794) -> Value {
795 let mut obj = json!({
796 "Id": id,
797 "Name": name,
798 "ARN": arn,
799 "LockToken": lock_token,
800 });
801 if let Some(d) = description {
802 obj.as_object_mut()
803 .unwrap()
804 .insert("Description".to_string(), Value::String(d.to_string()));
805 }
806 obj
807}
808
809fn regex_set_detail_json(set: &RegexPatternSet) -> Value {
810 let mut obj = json!({
811 "Id": set.id,
812 "Name": set.name,
813 "ARN": set.arn,
814 "RegularExpressionList": set.regular_expressions,
815 });
816 if let Some(d) = &set.description {
817 obj.as_object_mut()
818 .unwrap()
819 .insert("Description".to_string(), Value::String(d.clone()));
820 }
821 obj
822}
823
824#[cfg(test)]
825mod arn_norm_tests {
826 use super::normalize_resource_arn;
827
828 #[test]
829 fn elb_listener_arn_collapses_to_load_balancer_arn() {
830 let listener =
831 "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/web/abc/xyz";
832 assert_eq!(
833 normalize_resource_arn(listener),
834 "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/web/abc"
835 );
836 }
837
838 #[test]
839 fn elb_load_balancer_arn_passes_through() {
840 let lb = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/web/abc";
841 assert_eq!(normalize_resource_arn(lb), lb);
842 }
843
844 #[test]
845 fn nlb_listener_arn_collapses_to_network_load_balancer_arn() {
846 let listener =
847 "arn:aws:elasticloadbalancing:eu-west-1:123456789012:listener/net/wire/abc/xyz";
848 assert_eq!(
849 normalize_resource_arn(listener),
850 "arn:aws:elasticloadbalancing:eu-west-1:123456789012:loadbalancer/net/wire/abc"
851 );
852 }
853
854 #[test]
855 fn non_elbv2_arn_passes_through() {
856 let apigw = "arn:aws:apigateway:us-east-1::/restapis/abc/stages/prod";
857 assert_eq!(normalize_resource_arn(apigw), apigw);
858 let cog = "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_xxx";
859 assert_eq!(normalize_resource_arn(cog), cog);
860 }
861}
862
863#[cfg(test)]
864mod managed_rule_set_validation_tests {
865 use super::*;
866
867 fn make_body(fields: &[(&str, &str)]) -> Value {
868 let mut m = serde_json::Map::new();
869 for (k, v) in fields {
870 m.insert(k.to_string(), Value::String(v.to_string()));
871 }
872 Value::Object(m)
873 }
874
875 #[test]
876 fn put_managed_rule_set_versions_rejects_empty_name() {
877 let body = make_body(&[
878 ("Name", ""),
879 ("Id", "id"),
880 ("LockToken", "tok"),
881 ("Scope", "REGIONAL"),
882 ("RecommendedVersion", "1.0"),
883 ]);
884 let svc = Wafv2Service::default();
885 let req = AwsRequest {
886 service: "wafv2".into(),
887 action: "PutManagedRuleSetVersions".into(),
888 method: http::Method::POST,
889 raw_path: "/".into(),
890 raw_query: String::new(),
891 path_segments: Vec::new(),
892 query_params: std::collections::HashMap::new(),
893 headers: http::HeaderMap::new(),
894 body: serde_json::to_vec(&body).unwrap().into(),
895 body_stream: parking_lot::Mutex::new(None),
896 account_id: "123456789012".into(),
897 region: "us-east-1".into(),
898 request_id: "r".into(),
899 is_query_protocol: false,
900 access_key_id: None,
901 principal: None,
902 };
903 let res = svc.put_managed_rule_set_versions(&req);
904 assert!(res.is_err());
905 assert_eq!(res.err().unwrap().code(), "ValidationException");
906 }
907
908 #[test]
909 fn put_managed_rule_set_versions_rejects_long_name() {
910 let body = make_body(&[
911 ("Name", &"x".repeat(129)),
912 ("Id", "id"),
913 ("LockToken", "tok"),
914 ("Scope", "REGIONAL"),
915 ("RecommendedVersion", "1.0"),
916 ]);
917 let svc = Wafv2Service::default();
918 let req = AwsRequest {
919 service: "wafv2".into(),
920 action: "PutManagedRuleSetVersions".into(),
921 method: http::Method::POST,
922 raw_path: "/".into(),
923 raw_query: String::new(),
924 path_segments: Vec::new(),
925 query_params: std::collections::HashMap::new(),
926 headers: http::HeaderMap::new(),
927 body: serde_json::to_vec(&body).unwrap().into(),
928 body_stream: parking_lot::Mutex::new(None),
929 account_id: "123456789012".into(),
930 region: "us-east-1".into(),
931 request_id: "r".into(),
932 is_query_protocol: false,
933 access_key_id: None,
934 principal: None,
935 };
936 let res = svc.put_managed_rule_set_versions(&req);
937 assert!(res.is_err());
938 assert_eq!(res.err().unwrap().code(), "ValidationException");
939 }
940
941 #[test]
942 fn update_managed_rule_set_version_expiry_date_rejects_missing_timestamp() {
943 let body = make_body(&[
944 ("Name", "name"),
945 ("Id", "id"),
946 ("LockToken", "tok"),
947 ("Scope", "REGIONAL"),
948 ("VersionToExpire", "1.0"),
949 ]);
950 let svc = Wafv2Service::default();
951 let req = AwsRequest {
952 service: "wafv2".into(),
953 action: "UpdateManagedRuleSetVersionExpiryDate".into(),
954 method: http::Method::POST,
955 raw_path: "/".into(),
956 raw_query: String::new(),
957 path_segments: Vec::new(),
958 query_params: std::collections::HashMap::new(),
959 headers: http::HeaderMap::new(),
960 body: serde_json::to_vec(&body).unwrap().into(),
961 body_stream: parking_lot::Mutex::new(None),
962 account_id: "123456789012".into(),
963 region: "us-east-1".into(),
964 request_id: "r".into(),
965 is_query_protocol: false,
966 access_key_id: None,
967 principal: None,
968 };
969 let res = svc.update_managed_rule_set_version_expiry_date(&req);
970 assert!(res.is_err());
971 assert_eq!(res.err().unwrap().code(), "WAFInvalidParameterException");
972 }
973
974 #[test]
978 fn paginate_checked_rejects_invalid_token() {
979 use fakecloud_core::pagination::paginate_checked;
980 let items: Vec<i32> = (0..5).collect();
981 assert!(paginate_checked(&items, Some("not-a-valid-token"), 3).is_err());
982 assert!(paginate_checked(&items, Some("2"), 3).is_ok());
983 assert!(paginate_checked(&items, None, 3).is_ok());
984 }
985}