Skip to content

Commit

Permalink
New module for creating Cloudfront header policies… (ansible-collecti…
Browse files Browse the repository at this point in the history
…ons#925)

New module for creating Cloudfront header policies…

.. used for response headers
SUMMARY
New Cloudfront module for CF response headers policies, see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html
This is still a relavily new feature, see https://aws.amazon.com/de/blogs/networking-and-content-delivery/amazon-cloudfront-introduces-response-headers-policies/
ISSUE TYPE

New Module Pull Request

COMPONENT NAME
cloudfront_response_headers_policy.py

Reviewed-by: Mark Woolley <[email protected]>
Reviewed-by: Stefan Horning <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: Markus Bergholz <[email protected]>
  • Loading branch information
stefanhorning authored and abikouo committed Sep 18, 2023
1 parent 7af46c8 commit c71ac90
Showing 1 changed file with 291 additions and 0 deletions.
291 changes: 291 additions & 0 deletions cloudfront_response_headers_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
#!/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.2.0
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 U(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
'''

EXAMPLES = '''
- name: Creationg a Cloudfront header policy using all predefined header features and a custom header for 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
- name: Delete header policy
community.aws.cloudfront_response_headers_policy:
name: my-header-policy
state: absent
'''

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, BotoCoreError
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')
self.check_mode = module.check_mode

def find_response_headers_policy(self, name):
try:
policies = self.client.list_response_headers_policies()['ResponseHeadersPolicyList']['Items']

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'])
break
else:
matching_policy = None

return matching_policy
except (ParamValidationError, ClientError, BotoCoreError) as e:
self.module.fail_json_aws(e, msg="Error fetching policy information")

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 self.check_mode:
self.module.exit_json(changed=True, response_headers_policy=camel_dict_to_snake_dict(config))

if matching_policy is None:
try:
result = self.client.create_response_headers_policy(ResponseHeadersPolicyConfig=config)
changed = True
except (ParamValidationError, ClientError, BotoCoreError) as e:
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)

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, BotoCoreError) as e:
self.module.fail_json_aws(e, msg="Updating creating policy")

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

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

if matching_policy is 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']
if self.check_mode:
result = {}
else:
try:
result = self.client.delete_response_headers_policy(Id=policy_id, IfMatch=etag)
except (ParamValidationError, ClientError, BotoCoreError) as e:
self.module.fail_json_aws(e, msg="Error deleting policy")

self.module.exit_json(changed=True, **camel_dict_to_snake_dict(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'),
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=True)

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()

0 comments on commit c71ac90

Please sign in to comment.