forked from ansible-collections/amazon.aws
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New module for creating Cloudfront header policies… (ansible-collecti…
…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
1 parent
7af46c8
commit c71ac90
Showing
1 changed file
with
291 additions
and
0 deletions.
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
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() |