Skip to content
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

New module for creating Cloudfront header policies… #925

282 changes: 282 additions & 0 deletions plugins/modules/cloudfront_response_headers_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
#!/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)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


DOCUMENTATION = '''
---
version_added: 3.0.1
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved
module: cloudfront_response_headers_policy

short_description: Create, update and delete response headers policies to be used in a Cloudfront distribution

description:
- Create, update and delete response headers policies to be used in a Cloudfront distribution
for inserting custom headers. See docs at https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudfront.html#CloudFront.Client.create_response_headers_policy

author: Stefan Horning (@stefanhorning)

extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2


options:
state:
description: Decides if the named policy should be absent or present
choices:
- present
- absent
default: present
type: str
name:
description: Name of the policy
required: true
type: str
comment:
description: Description of the policy
required: false
type: str
cors_config:
description: CORS header config block
required: false
default: {}
type: dict
security_headers_config:
description: Security headers config block. For headers suchs as XSS-Protection, Content-Security-Policy or Strict-Transport-Security
required: false
default: {}
type: dict
custom_headers_config:
description: Custom headers config block. Define your own list of headers and values as a list
required: false
default: {}
type: dict

notes:
- Does not support check mode.
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved

'''

EXAMPLES = '''
- name: Creationg a Cloudfront header policy using all predefined header features and a custom header to demonstration
community.aws.cloudfront_response_headers_policy:
name: my-header-policy
comment: My header policy for all the headers
cors_config:
access_control_allow_origins:
items:
- 'https://foo.com/bar'
- 'https://bar.com/foo'
access_control_allow_headers:
items:
- 'X-Session-Id'
access_control_allow_methods:
items:
- GET
- OPTIONS
- HEAD
access_control_allow_credentials: true
access_control_expose_headers:
items:
- 'X-Session-Id'
access_control_max_age_sec: 1800
origin_override: true
security_headers_config:
xss_protection:
protection: true
report_uri: 'https://my.report-uri.com/foo/bar'
override: true
frame_options:
frame_option: 'SAMEORIGIN'
override: true
referrer_policy:
referrer_policy: 'same-origin'
override: true
content_security_policy:
content_security_policy: "frame-ancestors 'none'; report-uri https://my.report-uri.com/r/d/csp/enforce;"
override: true
content_type_options:
override: true
strict_transport_security:
include_subdomains: true
preload: true
access_control_max_age_sec: 63072000
override: true
custom_headers_config:
items:
- { header: 'X-Test-Header', value: 'Foo', override: true }
state: present
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved
'''

RETURN = '''
response_headers_policy:
description: The policy's information
returned: success
type: complex
contains:
id:
description: ID of the policy
returned: always
type: str
sample: '10a45b52-630e-4b7c-77c6-205f06df0462'
last_modified_time:
description: Timestamp of last modification of policy
returned: always
type: str
sample: '2022-02-04T13:23:27.304000+00:00'
response_headers_policy_config:
description: The response headers config dict containing all the headers configured
returned: always
type: complex
contains:
name:
description: Name of the policy
type: str
returned: always
sample: my-header-policy
'''

try:
from botocore.exceptions import ClientError, ParamValidationError
except ImportError:
pass # caught by imported AnsibleAWSModule

from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict, snake_dict_to_camel_dict
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
import datetime

class CloudfrontResponseHeadersPolicyService(object):

def __init__(self, module):
self.module = module
self.client = module.client('cloudfront')


def find_response_headers_policy(self, name):
policies = self.client.list_response_headers_policies()['ResponseHeadersPolicyList']['Items']
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved

for policy in policies:
if policy['ResponseHeadersPolicy']['ResponseHeadersPolicyConfig']['Name'] == name:
policy_id = policy['ResponseHeadersPolicy']['Id']
# as the list_ request does not contain the Etag (which we need), we need to do another get_ request here
matching_policy = self.client.get_response_headers_policy(Id=policy['ResponseHeadersPolicy']['Id'])
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved
break
else:
matching_policy = None

return matching_policy


def create_response_header_policy(self, name, comment, cors_config, security_headers_config, custom_headers_config):
cors_config = snake_dict_to_camel_dict(cors_config, capitalize_first=True)
security_headers_config = snake_dict_to_camel_dict(security_headers_config, capitalize_first=True)

# Little helper for turning xss_protection into XSSProtection and not into XssProtection
if 'XssProtection' in security_headers_config:
security_headers_config['XSSProtection'] = security_headers_config.pop('XssProtection')

custom_headers_config = snake_dict_to_camel_dict(custom_headers_config, capitalize_first=True)

config = {
'Name': name,
'Comment': comment,
'CorsConfig': self.insert_quantities(cors_config),
'SecurityHeadersConfig': security_headers_config,
'CustomHeadersConfig': self.insert_quantities(custom_headers_config)
}

config = {k: v for k, v in config.items() if v}

matching_policy = self.find_response_headers_policy(name)

changed = False

if matching_policy == None:
try:
result = self.client.create_response_headers_policy(ResponseHeadersPolicyConfig=config)
changed = True
except (ParamValidationError, ClientError) as e:
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved
self.module.fail_json_aws(e, msg="Error creating policy")
else:
policy_id = matching_policy['ResponseHeadersPolicy']['Id']
etag = matching_policy['ETag']
try:
result = self.client.update_response_headers_policy(Id=policy_id, IfMatch=etag, ResponseHeadersPolicyConfig=config)
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved

changed_time = result['ResponseHeadersPolicy']['LastModifiedTime']
seconds = 3 # threshhold for returned timestamp age
seconds_ago = (datetime.datetime.now(changed_time.tzinfo) - datetime.timedelta(0,seconds))

# consider change made by this execution of the module if returned timestamp was very recent
if changed_time > seconds_ago:
changed = True
except (ParamValidationError, ClientError) as e:
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved
self.module.fail_json_aws(e, msg="Error creating policy")

self.module.exit_json(changed=changed, **camel_dict_to_snake_dict(result))
return result



def delete_response_header_policy(self, name):
matching_policy = self.find_response_headers_policy(name)

if matching_policy == None:
self.module.exit_json(msg="Didn't find a matching policy by that name, not deleting")
else:
policy_id = matching_policy['ResponseHeadersPolicy']['Id']
etag = matching_policy['ETag']
result = self.client.delete_response_headers_policy(Id=policy_id, IfMatch=etag)

self.module.exit_json(changed=True, **camel_dict_to_snake_dict(result))
return result


# Inserts a Quantity field into dicts with a list ('Items')
@staticmethod
def insert_quantities(dict_with_items):
# Items on top level case
if 'Items' in dict_with_items and isinstance(dict_with_items['Items'], list):
dict_with_items['Quantity'] = len(dict_with_items['Items'])

# Items on second level case
for k, v in dict_with_items.items():
if isinstance(v, dict) and 'Items' in v:
v['Quantity'] = len(v['Items'])

return dict_with_items


def main():
argument_spec = dict(
name=dict(required=True, type='str'),
comment=dict(type='str', default=''),
stefanhorning marked this conversation as resolved.
Show resolved Hide resolved
cors_config=dict(type='dict', default=dict()),
security_headers_config=dict(type='dict', default=dict()),
custom_headers_config=dict(type='dict', default=dict()),
state=dict(choices=['present', 'absent'], type='str', default='present'),
)

module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=False)

name = module.params.get('name')
comment = module.params.get('comment')
cors_config = module.params.get('cors_config')
security_headers_config = module.params.get('security_headers_config')
custom_headers_config = module.params.get('custom_headers_config')
state = module.params.get('state')

service = CloudfrontResponseHeadersPolicyService(module)

if state == 'absent':
service.delete_response_header_policy(name)
else:
service.create_response_header_policy(name, comment, cors_config, security_headers_config, custom_headers_config)


if __name__ == '__main__':
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cloud/aws
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dependencies: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---

- name: Integration testing for the cloudfront_response_headers_policy module
module_defaults:
group/aws:
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token | default(omit) }}'
region: '{{ aws_region }}'
block:

- name: Create a simple header policy
cloudfront_response_headers_policy:
name: "{{ resource_prefix }}-my-header-policy"
comment: Created by Ansible test
security_headers_config:
content_type_options:
override: true
state: present
register: create_result

- name: Assert creation withouth errors and return values
assert:
that:
- create_result is changed
- create_result is not failed
- create_result.response_headers_policy.response_headers_policy_config.name == "{{ resource_prefix }}-my-header-policy"

- name: Rerun same task to ensure idempotence
cloudfront_response_headers_policy:
name: "{{ resource_prefix }}-my-header-policy"
comment: Created by Ansible test
security_headers_config:
content_type_options:
override: true
state: present
register: rerun_result

- name: Assert no change and no errors
assert:
that:
- rerun_result is not changed
- rerun_result is not failed

- name: Update existing policy with more header configs
cloudfront_response_headers_policy:
name: "{{ resource_prefix }}-my-header-policy"
comment: Created by Ansible test
cors_config:
access_control_allow_origins:
items:
- 'https://foo.com/bar'
- 'https://bar.com/foo'
access_control_allow_methods:
items:
- OPTIONS
- HEAD
- GET
access_control_max_age_sec: 1800
origin_override: true
security_headers_config:
content_type_options:
override: true
custom_headers_config:
items:
- { header: 'X-Test-Header', value: 'Foo', override: true }
state: present
register: update_result

- name: Assert update and updated return values
assert:
that:
- update_result is changed
- update_result.response_headers_policy.response_headers_policy_config.cors_config.access_control_max_age_sec == 1800

- name: Ensure policy is deleted
cloudfront_response_headers_policy:
name: "{{ resource_prefix }}-my-header-policy"
comment: Created by Ansible test
state: absent
register: delete_result

- name: Assert deletion without errors
assert:
that:
- delete_result is changed
- delete_result is not failed
- update_result.response_headers_policy is undefined

always:

- name: Ensure policy is deleted
cloudfront_response_headers_policy:
name: "{{ resource_prefix }}-my-header-policy"
state: absent
ignore_errors: true