New aws_waf_web_acl module (#33212)

* Add aws_waf_web_acl rule

* Add new aws_waf_web_acl to CHANGELOG
This commit is contained in:
Will Thames 2018-02-07 22:06:15 +10:00 committed by Sloane Hertel
parent 1a8ed08e6c
commit fcd09e2f02
3 changed files with 430 additions and 0 deletions

View file

@ -109,6 +109,7 @@ See [Porting Guide](http://docs.ansible.com/ansible/devel/porting_guides.html) f
* aws_ssm_parameter_store
* aws_waf_condition
* aws_waf_rule
* aws_waf_web_acl
* cloudfront_distribution
* cloudfront_invalidation
* cloudfront_origin_access_identity

View file

@ -0,0 +1,290 @@
#!/usr/bin/python
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
module: aws_waf_web_acl
short_description: create and delete WAF Web ACLs
description:
- Read the AWS documentation for WAF
U(https://aws.amazon.com/documentation/waf/)
version_added: "2.5"
author:
- Mike Mochan (@mmochan)
- Will Thames (@willthames)
extends_documentation_fragment:
- aws
- ec2
options:
name:
description: Name of the Web Application Firewall ACL to manage
required: yes
default_action:
description: The action that you want AWS WAF to take when a request doesn't
match the criteria specified in any of the Rule objects that are associated with the WebACL
choices:
- block
- allow
- count
state:
description: whether the Web ACL should be present or absent
choices:
- present
- absent
default: present
metric_name:
description:
- A friendly name or description for the metrics for this WebACL
- The name can contain only alphanumeric characters (A-Z, a-z, 0-9); the name can't contain whitespace.
- You can't change metric_name after you create the WebACL
- Metric name will default to I(name) with disallowed characters stripped out
rules:
description:
- A list of rules that the Web ACL will enforce.
- Each rule must contain I(name), I(action), I(priority) keys.
- Priorities must be unique, but not necessarily consecutive. Lower numbered priorities are evalauted first.
- The I(type) key can be passed as C(rate_based), it defaults to C(regular)
purge_rules:
description: Whether to remove rules that aren't passed with C(rules). Defaults to false
'''
EXAMPLES = '''
- name: create web ACL
aws_waf_web_acl:
name: my_web_acl
rules:
- name: my_rule
priority: 1
action: block
default_action: block
purge_rules: yes
state: present
- name: delete the web acl
aws_waf_web_acl:
name: my_web_acl
state: absent
'''
RETURN = '''
web_acl:
description: contents of the Web ACL
returned: always
type: complex
contains:
default_action:
description: Default action taken by the Web ACL if no rules match
returned: always
type: dict
sample:
type: BLOCK
metric_name:
description: Metric name used as an identifier
returned: always
type: string
sample: mywebacl
name:
description: Friendly name of the Web ACL
returned: always
type: string
sample: my web acl
rules:
description: List of rules
returned: always
type: complex
contains:
action:
description: Action taken by the WAF when the rule matches
returned: always
type: complex
sample:
type: ALLOW
priority:
description: priority number of the rule (lower numbers are run first)
returned: always
type: int
sample: 2
rule_id:
description: Rule ID
returned: always
type: string
sample: a6fc7ab5-287b-479f-8004-7fd0399daf75
type:
description: Type of rule (either REGULAR or RATE_BASED)
returned: always
type: string
sample: REGULAR
web_acl_id:
description: Unique identifier of Web ACL
returned: always
type: string
sample: 10fff965-4b6b-46e2-9d78-24f6d2e2d21c
'''
try:
import botocore
except ImportError:
pass # handled by AnsibleAWSModule
import re
from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, camel_dict_to_snake_dict
from ansible.module_utils.aws.waf import list_rules_with_backoff, list_web_acls_with_backoff, get_change_token
def get_web_acl_by_name(client, module, name):
acls = [d['WebACLId'] for d in list_web_acls(client, module) if d['Name'] == name]
if acls:
return acls[0]
else:
return acls
def create_rule_lookup(client, module):
try:
rules = list_rules_with_backoff(client)
return dict((rule['Name'], rule) for rule in rules)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not list rules')
def get_web_acl(client, module, web_acl_id):
try:
return client.get_web_acl(WebACLId=web_acl_id)['WebACL']
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not get Web ACL with id %s' % web_acl_id)
def list_web_acls(client, module,):
try:
return list_web_acls_with_backoff(client)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not get Web ACLs')
def find_and_update_web_acl(client, module, web_acl_id):
acl = get_web_acl(client, module, web_acl_id)
rule_lookup = create_rule_lookup(client, module)
existing_rules = acl['Rules']
desired_rules = [{'RuleId': rule_lookup[rule['name']]['RuleId'],
'Priority': rule['priority'],
'Action': {'Type': rule['action'].upper()},
'Type': rule.get('type', 'regular').upper()}
for rule in module.params['rules']]
missing = [rule for rule in desired_rules if rule not in existing_rules]
extras = []
if module.params['purge_rules']:
extras = [rule for rule in existing_rules if rule not in desired_rules]
insertions = [format_for_update(rule, 'INSERT') for rule in missing]
deletions = [format_for_update(rule, 'DELETE') for rule in extras]
changed = bool(insertions + deletions)
if changed:
try:
client.update_web_acl(
WebACLId=acl['WebACLId'],
ChangeToken=get_change_token(client, module),
Updates=insertions + deletions,
DefaultAction=acl['DefaultAction']
)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not update Web ACL')
acl = get_web_acl(client, module, web_acl_id)
return changed, acl
def format_for_update(rule, action):
return dict(
Action=action,
ActivatedRule=dict(
Priority=rule['Priority'],
RuleId=rule['RuleId'],
Action=dict(
Type=rule['Action']['Type']
)
)
)
def remove_rules_from_web_acl(client, module, web_acl_id):
acl = get_web_acl(client, module, web_acl_id)
deletions = [format_for_update(rule, 'DELETE') for rule in acl['Rules']]
try:
client.update_web_acl(WebACLId=acl['WebACLId'], ChangeToken=get_change_token(client, module),
Updates=deletions, DefaultAction=acl['DefaultAction'])
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not remove rule')
def ensure_web_acl_present(client, module):
changed = False
result = None
name = module.params['name']
web_acl_id = get_web_acl_by_name(client, module, name)
if web_acl_id:
(changed, result) = find_and_update_web_acl(client, module, web_acl_id)
else:
metric_name = module.params['metric_name']
if not metric_name:
metric_name = re.sub(r'[^A-Za-z0-9]', '', module.params['name'])
default_action = module.params['default_action'].upper()
try:
new_web_acl = client.create_web_acl(Name=name, MetricName=metric_name,
DefaultAction={'Type': default_action},
ChangeToken=get_change_token(client, module))
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not create Web ACL')
(changed, result) = find_and_update_web_acl(client, module, new_web_acl['WebACL']['WebACLId'])
return changed, result
def ensure_web_acl_absent(client, module):
web_acl_id = get_web_acl_by_name(client, module, module.params['name'])
if web_acl_id:
web_acl = get_web_acl(client, module, web_acl_id)
if web_acl['Rules']:
remove_rules_from_web_acl(client, module, web_acl_id)
try:
client.delete_web_acl(WebACLId=web_acl_id, ChangeToken=get_change_token(client, module))
return True, {}
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg='Could not delete Web ACL')
return False, {}
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
name=dict(required=True),
default_action=dict(choices=['block', 'allow', 'count']),
metric_name=dict(),
state=dict(default='present', choices=['present', 'absent']),
rules=dict(type='list'),
purge_rules=dict(type='bool', default=False)
),
)
module = AnsibleAWSModule(argument_spec=argument_spec,
required_if=[['state', 'present', ['default_action', 'rules']]])
state = module.params.get('state')
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
client = boto3_conn(module, conn_type='client', resource='waf', region=region, endpoint=ec2_url, **aws_connect_kwargs)
if state == 'present':
(changed, results) = ensure_web_acl_present(client, module)
else:
(changed, results) = ensure_web_acl_absent(client, module)
module.exit_json(changed=changed, web_acl=camel_dict_to_snake_dict(results))
if __name__ == '__main__':
main()

View file

@ -345,10 +345,149 @@
- remove_in_use_condition.failed
- "'Condition {{ resource_prefix }}_size_condition is in use' in remove_in_use_condition.msg"
##################################################
# aws_waf_web_acl tests
##################################################
- name: create web ACL
aws_waf_web_acl:
name: "{{ resource_prefix }}_web_acl"
rules:
- name: "{{ resource_prefix }}_rule"
priority: 1
action: block
default_action: block
purge_rules: yes
state: present
<<: *aws_connection_info
register: create_web_acl
- name: recreate web acl
aws_waf_web_acl:
name: "{{ resource_prefix }}_web_acl"
rules:
- name: "{{ resource_prefix }}_rule"
priority: 1
action: block
default_action: block
state: present
<<: *aws_connection_info
register: recreate_web_acl
- name: check web acl was not changed
assert:
that:
- not recreate_web_acl.changed
- recreate_web_acl.web_acl.rules|length == 1
- name: create a second WAF rule
aws_waf_rule:
name: "{{ resource_prefix }}_rule_2"
conditions:
- name: "{{ resource_prefix }}_ip_condition"
type: ip
negated: yes
- name: "{{ resource_prefix }}_sql_condition"
type: sql
negated: no
- name: "{{ resource_prefix }}_xss_condition"
type: xss
negated: no
<<: *aws_connection_info
- name: add a new rule to the web acl
aws_waf_web_acl:
name: "{{ resource_prefix }}_web_acl"
rules:
- name: "{{ resource_prefix }}_rule_2"
priority: 2
action: allow
default_action: block
state: present
<<: *aws_connection_info
register: web_acl_add_rule
- name: check that rule was added to the web acl
assert:
that:
- web_acl_add_rule.changed
- web_acl_add_rule.web_acl.rules|length == 2
- name: use purge rules to remove the first rule
aws_waf_web_acl:
name: "{{ resource_prefix }}_web_acl"
rules:
- name: "{{ resource_prefix }}_rule_2"
priority: 2
action: allow
purge_rules: yes
default_action: block
state: present
<<: *aws_connection_info
register: web_acl_add_rule
- name: check that rule was removed from the web acl
assert:
that:
- web_acl_add_rule.changed
- web_acl_add_rule.web_acl.rules|length == 1
- name: swap two rules of same priority
aws_waf_web_acl:
name: "{{ resource_prefix }}_web_acl"
rules:
- name: "{{ resource_prefix }}_rule"
priority: 2
action: allow
purge_rules: yes
default_action: block
state: present
<<: *aws_connection_info
register: web_acl_swap_rule
- name: attempt to delete the inuse first rule
aws_waf_rule:
name: "{{ resource_prefix }}_rule"
state: absent
<<: *aws_connection_info
ignore_errors: yes
register: remove_inuse_rule
- name: check that removing in-use rule fails
assert:
that:
- remove_inuse_rule.failed
- name: delete the web acl
aws_waf_web_acl:
name: "{{ resource_prefix }}_web_acl"
state: absent
<<: *aws_connection_info
register: delete_web_acl
- name: check that web acl was deleted
assert:
that:
- delete_web_acl.changed
- not delete_web_acl.web_acl
- name: delete the no longer in use first rule
aws_waf_rule:
name: "{{ resource_prefix }}_rule"
state: absent
<<: *aws_connection_info
always:
- debug:
msg: "****** TEARDOWN STARTS HERE ******"
- name: remove second WAF rule
aws_waf_rule:
name: "{{ resource_prefix }}_rule_2"
state: absent
<<: *aws_connection_info
ignore_errors: yes
- name: remove WAF rule
aws_waf_rule:
name: "{{ resource_prefix }}_rule"