-
Notifications
You must be signed in to change notification settings - Fork 403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add wafv2 modules #450
Merged
Merged
add wafv2 modules #450
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9ccf6d0
add wafv2 modules
markuman c8d4665
sanity fix and fragment integrationtest
markuman 7f02218
fix linting
markuman aa22c32
fix sanity
markuman 9dc70d0
add exceptions
markuman 58daed7
add botocore exceptions
markuman ce72329
incr retries
markuman 0a759c2
wait for alb
markuman 2d5790f
fix function name, fix missing NextMarker usage
markuman f0c6c9e
fix documentation and integrationtest
markuman 2cdc723
lowercase nextmarker
markuman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
try: | ||
from botocore.exceptions import ClientError, BotoCoreError | ||
except ImportError: | ||
pass # caught by AnsibleAWSModule | ||
|
||
|
||
def wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=None): | ||
# there is currently no paginator for wafv2 | ||
req_obj = { | ||
'Scope': scope, | ||
'Limit': 100 | ||
} | ||
if nextmarker: | ||
req_obj['NextMarker'] = nextmarker | ||
|
||
try: | ||
response = wafv2.list_web_acls(**req_obj) | ||
except (BotoCoreError, ClientError) as e: | ||
fail_json_aws(e, msg="Failed to list wafv2 web acl.") | ||
|
||
if response.get('NextMarker'): | ||
response['WebACLs'] += wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('WebACLs') | ||
return response | ||
|
||
|
||
def wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=None): | ||
# there is currently no paginator for wafv2 | ||
req_obj = { | ||
'Scope': scope, | ||
'Limit': 100 | ||
} | ||
if nextmarker: | ||
req_obj['NextMarker'] = nextmarker | ||
|
||
try: | ||
response = wafv2.list_rule_groups(**req_obj) | ||
except (BotoCoreError, ClientError) as e: | ||
fail_json_aws(e, msg="Failed to list wafv2 rule group.") | ||
|
||
if response.get('NextMarker'): | ||
response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('RuleGroups') | ||
return response | ||
|
||
|
||
def wafv2_snake_dict_to_camel_dict(a): | ||
retval = {} | ||
for item in a.keys(): | ||
if isinstance(a.get(item), dict): | ||
if 'Ip' in item: | ||
retval[item.replace('Ip', 'IP')] = wafv2_snake_dict_to_camel_dict(a.get(item)) | ||
elif 'Arn' == item: | ||
retval['ARN'] = wafv2_snake_dict_to_camel_dict(a.get(item)) | ||
else: | ||
retval[item] = wafv2_snake_dict_to_camel_dict(a.get(item)) | ||
elif isinstance(a.get(item), list): | ||
retval[item] = [] | ||
for idx in range(len(a.get(item))): | ||
retval[item].append(wafv2_snake_dict_to_camel_dict(a.get(item)[idx])) | ||
elif 'Ip' in item: | ||
retval[item.replace('Ip', 'IP')] = a.get(item) | ||
elif 'Arn' == item: | ||
retval['ARN'] = a.get(item) | ||
else: | ||
retval[item] = a.get(item) | ||
return retval | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, unit-test would be great for the others too :-). |
||
def nested_byte_values_to_strings(rule, keyname): | ||
""" | ||
currently valid nested byte values in statements array are | ||
- OrStatement | ||
- AndStatement | ||
- NotStatement | ||
""" | ||
if rule.get('Statement', {}).get(keyname): | ||
for idx in range(len(rule.get('Statement', {}).get(keyname, {}).get('Statements'))): | ||
if rule['Statement'][keyname]['Statements'][idx].get('ByteMatchStatement'): | ||
rule['Statement'][keyname]['Statements'][idx]['ByteMatchStatement']['SearchString'] = \ | ||
rule.get('Statement').get(keyname).get('Statements')[idx].get('ByteMatchStatement').get('SearchString').decode('utf-8') | ||
|
||
return rule | ||
|
||
|
||
def byte_values_to_strings_before_compare(rules): | ||
for idx in range(len(rules)): | ||
if rules[idx].get('Statement', {}).get('ByteMatchStatement', {}).get('SearchString'): | ||
rules[idx]['Statement']['ByteMatchStatement']['SearchString'] = \ | ||
rules[idx].get('Statement').get('ByteMatchStatement').get('SearchString').decode('utf-8') | ||
|
||
else: | ||
for statement in ['AndStatement', 'OrStatement', 'NotStatement']: | ||
if rules[idx].get('Statement', {}).get(statement): | ||
rules[idx] = nested_byte_values_to_strings(rules[idx], statement) | ||
|
||
return rules | ||
|
||
|
||
def compare_priority_rules(existing_rules, requested_rules, purge_rules, state): | ||
diff = False | ||
existing_rules = sorted(existing_rules, key=lambda k: k['Priority']) | ||
existing_rules = byte_values_to_strings_before_compare(existing_rules) | ||
requested_rules = sorted(requested_rules, key=lambda k: k['Priority']) | ||
|
||
if purge_rules and state == 'present': | ||
merged_rules = requested_rules | ||
if len(existing_rules) == len(requested_rules): | ||
for idx in range(len(existing_rules)): | ||
if existing_rules[idx] != requested_rules[idx]: | ||
diff = True | ||
break | ||
else: | ||
diff = True | ||
|
||
else: | ||
# find same priority rules | ||
# * pop same priority rule from existing rule | ||
# * compare existing rule | ||
merged_rules = [] | ||
ex_idx_pop = [] | ||
for existing_idx in range(len(existing_rules)): | ||
for requested_idx in range(len(requested_rules)): | ||
if existing_rules[existing_idx].get('Priority') == requested_rules[requested_idx].get('Priority'): | ||
if state == 'present': | ||
ex_idx_pop.append(existing_idx) | ||
if existing_rules[existing_idx] != requested_rules[requested_idx]: | ||
diff = True | ||
elif existing_rules[existing_idx] == requested_rules[requested_idx]: | ||
ex_idx_pop.append(existing_idx) | ||
diff = True | ||
|
||
prev_count = len(existing_rules) | ||
for idx in ex_idx_pop: | ||
existing_rules.pop(idx) | ||
|
||
if state == 'present': | ||
merged_rules = existing_rules + requested_rules | ||
|
||
if len(merged_rules) != prev_count: | ||
diff = True | ||
else: | ||
merged_rules = existing_rules | ||
|
||
return diff, merged_rules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
#!/usr/bin/python | ||
# Copyright: Ansible Project | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
from __future__ import absolute_import, division, print_function | ||
__metaclass__ = type | ||
|
||
|
||
DOCUMENTATION = ''' | ||
--- | ||
module: wafv2_resources | ||
version_added: 1.5.0 | ||
author: | ||
- "Markus Bergholz (@markuman)" | ||
short_description: wafv2_web_acl | ||
description: | ||
- Apply or remove wafv2 to other aws resources. | ||
requirements: | ||
- boto3 | ||
- botocore | ||
options: | ||
state: | ||
description: | ||
- Whether the rule is present or absent. | ||
choices: ["present", "absent"] | ||
required: true | ||
type: str | ||
name: | ||
description: | ||
- The name of the web acl. | ||
type: str | ||
scope: | ||
description: | ||
- Scope of waf | ||
choices: ["CLOUDFRONT","REGIONAL"] | ||
type: str | ||
arn: | ||
description: | ||
- AWS resources (ALB, API Gateway or AppSync GraphQL API) ARN | ||
type: str | ||
required: true | ||
|
||
extends_documentation_fragment: | ||
- amazon.aws.aws | ||
- amazon.aws.ec2 | ||
|
||
''' | ||
|
||
EXAMPLES = ''' | ||
- name: add test alb to waf string03 | ||
community.aws.wafv2_resources: | ||
name: string03 | ||
scope: REGIONAL | ||
state: present | ||
arn: "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933" | ||
''' | ||
|
||
RETURN = """ | ||
resource_arns: | ||
description: Current resources where the wafv2 is applied on | ||
sample: | ||
- "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933" | ||
returned: Always, as long as the wafv2 exists | ||
type: list | ||
""" | ||
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters | ||
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list | ||
from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls | ||
|
||
try: | ||
from botocore.exceptions import ClientError, BotoCoreError | ||
except ImportError: | ||
pass # caught by AnsibleAWSModule | ||
|
||
|
||
def get_web_acl(wafv2, name, scope, id, fail_json_aws): | ||
try: | ||
response = wafv2.get_web_acl( | ||
Name=name, | ||
Scope=scope, | ||
Id=id | ||
) | ||
except (BotoCoreError, ClientError) as e: | ||
fail_json_aws(e, msg="Failed to get wafv2 web acl.") | ||
return response | ||
|
||
|
||
def list_wafv2_resources(wafv2, arn, fail_json_aws): | ||
try: | ||
response = wafv2.list_resources_for_web_acl( | ||
WebACLArn=arn | ||
) | ||
except (BotoCoreError, ClientError) as e: | ||
fail_json_aws(e, msg="Failed to list wafv2 web acl.") | ||
return response | ||
|
||
|
||
def add_wafv2_resources(wafv2, waf_arn, arn, fail_json_aws): | ||
try: | ||
response = wafv2.associate_web_acl( | ||
WebACLArn=waf_arn, | ||
ResourceArn=arn | ||
) | ||
except (BotoCoreError, ClientError) as e: | ||
fail_json_aws(e, msg="Failed to add wafv2 web acl.") | ||
return response | ||
|
||
|
||
def remove_resources(wafv2, arn, fail_json_aws): | ||
try: | ||
response = wafv2.disassociate_web_acl( | ||
ResourceArn=arn | ||
) | ||
except (BotoCoreError, ClientError) as e: | ||
fail_json_aws(e, msg="Failed to remove wafv2 web acl.") | ||
return response | ||
|
||
|
||
def main(): | ||
|
||
arg_spec = dict( | ||
state=dict(type='str', required=True, choices=['present', 'absent']), | ||
name=dict(type='str'), | ||
scope=dict(type='str', choices=['CLOUDFRONT', 'REGIONAL']), | ||
arn=dict(type='str', required=True) | ||
) | ||
|
||
module = AnsibleAWSModule( | ||
argument_spec=arg_spec, | ||
supports_check_mode=True, | ||
required_if=[['state', 'present', ['name', 'scope']]] | ||
) | ||
|
||
state = module.params.get("state") | ||
name = module.params.get("name") | ||
scope = module.params.get("scope") | ||
arn = module.params.get("arn") | ||
check_mode = module.check_mode | ||
|
||
wafv2 = module.client('wafv2') | ||
|
||
# check if web acl exists | ||
|
||
response = wafv2_list_web_acls(wafv2, scope, module.fail_json_aws) | ||
|
||
id = None | ||
retval = {} | ||
change = False | ||
|
||
for item in response.get('WebACLs'): | ||
if item.get('Name') == name: | ||
id = item.get('Id') | ||
|
||
if id: | ||
existing_acl = get_web_acl(wafv2, name, scope, id, module.fail_json_aws) | ||
waf_arn = existing_acl.get('WebACL').get('ARN') | ||
|
||
retval = list_wafv2_resources(wafv2, waf_arn, module.fail_json_aws) | ||
|
||
if state == 'present': | ||
if retval: | ||
if arn not in retval.get('ResourceArns'): | ||
change = True | ||
if not check_mode: | ||
retval = add_wafv2_resources(wafv2, waf_arn, arn, module.fail_json_aws) | ||
|
||
elif state == 'absent': | ||
if retval: | ||
if arn in retval.get('ResourceArns'): | ||
change = True | ||
if not check_mode: | ||
retval = remove_resources(wafv2, arn, module.fail_json_aws) | ||
|
||
module.exit_json(changed=change, **camel_dict_to_snake_dict(retval)) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I suggest to add a unit-test for this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm basically it is already covered by the integration test.
I've added different rules circumstances that must be covered by those functions and that must not be covered by those functions.
nested_byte_values_to_strings
andbyte_values_to_strings_before_compare
byte_match_statement
: https://github.com/ansible-collections/community.aws/pull/450/files#diff-766b578e1e76effb51404af767b665a1dbd0a00d378ac1645cbdbeb1916b006fR21or_statement
: https://github.com/ansible-collections/community.aws/pull/450/files#diff-766b578e1e76effb51404af767b665a1dbd0a00d378ac1645cbdbeb1916b006fR237wafv2_snake_dict_to_camel_dict()
ip_set_reference_statement
becomesIPSetReferenceStatement
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I know and this is the reason why I put
suggest
in bold. PLEASE, Don't force yourself to do it!I see two reasons to write unit-tests in your case:
Well, that's 3 reasons actually ^^.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what it's worth you should be able to copy most of the unit-test framework for these transforms from https://github.com/ansible-collections/amazon.aws/blob/main/tests/unit/module_utils/core/test_scrub_none_parameters.py