diff --git a/changelogs/fragments/171-s3_bucket-public_access.yml b/changelogs/fragments/171-s3_bucket-public_access.yml new file mode 100644 index 00000000000..1a179a21e2c --- /dev/null +++ b/changelogs/fragments/171-s3_bucket-public_access.yml @@ -0,0 +1,2 @@ +minor_changes: +- s3_bucket - Add support for managing the ``public_access`` settings (https://github.com/ansible-collections/amazon.aws/pull/171). diff --git a/plugins/modules/s3_bucket.py b/plugins/modules/s3_bucket.py index 2cc9021b6b5..f04a6fb4a18 100644 --- a/plugins/modules/s3_bucket.py +++ b/plugins/modules/s3_bucket.py @@ -89,6 +89,37 @@ description: KMS master key ID to use for the default encryption. This parameter is allowed if encryption is aws:kms. If not specified then it will default to the AWS provided KMS key. type: str + public_access: + description: + - Configure public access block for S3 bucket + - This option cannot be used together with 'delete_public_access' + suboptions: + block_public_acls: + description: Sets BlockPublicAcls value + type: bool + default: False + block_public_policy: + description: Sets BlockPublicPolicy value + type: bool + default: False + ignore_public_acls: + description: Sets IgnorePublicAcls value + type: bool + default: False + restrict_public_buckets: + description: Sets RestrictPublicAcls value + type: bool + default: False + type: dict + version_added: 1.3.0 + delete_public_access: + description: + - Delete public access block configuration from bucket + - This option cannot be used together with 'public_access' definition + default: false + type: bool + version_added: 1.3.0 + extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -153,6 +184,23 @@ name: mys3bucket state: present encryption: "aws:kms" + +# Create a bucket with public policy block configuration +- amazon.aws.s3_bucket: + name: mys3bucket + state: present + public_access: + BlockPublicAcls: true + IgnorePublicAcls: true + ## keys == 'false' can be ommited, undefined keys defaults to 'false' + # BlockPublicPolicy: false + # RestrictPublicBuckets: false + +# Delete public policy block from bucket +- amazon.aws.s3_bucket: + name: mys3bucket + state: present + delete_public_access: true ''' import json @@ -176,6 +224,7 @@ from ..module_utils.ec2 import boto3_tag_list_to_ansible_dict from ..module_utils.ec2 import compare_policies from ..module_utils.ec2 import get_aws_connection_info +from ..module_utils.ec2 import snake_dict_to_camel_dict def create_or_update_bucket(s3_client, module, location): @@ -188,6 +237,8 @@ def create_or_update_bucket(s3_client, module, location): versioning = module.params.get("versioning") encryption = module.params.get("encryption") encryption_key_id = module.params.get("encryption_key_id") + public_access = module.params.get("public_access") + delete_public_access = module.params.get("delete_public_access") changed = False result = {} @@ -356,6 +407,39 @@ def create_or_update_bucket(s3_client, module, location): result['encryption'] = current_encryption + # Public access clock configuration + current_public_access = {} + + # -- Create / Update public access block + if public_access is not None: + try: + current_public_access = get_bucket_public_access(s3_client, name) + except (ClientError, BotoCoreError) as err_public_access: + module.fail_json_aws(err_public_access, msg="Failed to get bucket public access configuration") + camel_public_block = snake_dict_to_camel_dict(public_access, capitalize_first=True) + + if current_public_access == camel_public_block: + result['public_access_block'] = current_public_access + else: + put_bucket_public_access(s3_client, name, camel_public_block) + changed = True + result['public_access_block'] = camel_public_block + + # -- Delete public access block + if delete_public_access: + try: + current_public_access = get_bucket_public_access(s3_client, name) + except (ClientError, BotoCoreError) as err_public_access: + module.fail_json_aws(err_public_access, msg="Failed to get bucket public access configuration") + + if current_public_access == {}: + result['public_access_block'] = current_public_access + else: + delete_bucket_public_access(s3_client, name) + changed = True + result['public_access_block'] = {} + + # Module exit module.exit_json(changed=changed, name=name, **result) @@ -487,6 +571,22 @@ def delete_bucket(s3_client, bucket_name): pass +@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) +def put_bucket_public_access(s3_client, bucket_name, public_acces): + ''' + Put new public access block to S3 bucket + ''' + s3_client.put_public_access_block(Bucket=bucket_name, PublicAccessBlockConfiguration=public_acces) + + +@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) +def delete_bucket_public_access(s3_client, bucket_name): + ''' + Delete public access block from S3 bucket + ''' + s3_client.delete_public_access_block(Bucket=bucket_name) + + def wait_policy_is_applied(module, s3_client, bucket_name, expected_policy, should_fail=True): for dummy in range(0, 12): try: @@ -580,6 +680,17 @@ def get_current_bucket_tags_dict(s3_client, bucket_name): return boto3_tag_list_to_ansible_dict(current_tags) +def get_bucket_public_access(s3_client, bucket_name): + ''' + Get current bucket public access block + ''' + try: + bucket_public_access_block = s3_client.get_public_access_block(Bucket=bucket_name) + return bucket_public_access_block['PublicAccessBlockConfiguration'] + except is_boto3_error_code('NoSuchPublicAccessBlockConfiguration'): + return {} + + def paginated_list(s3_client, **pagination_params): pg = s3_client.get_paginator('list_objects_v2') for page in pg.paginate(**pagination_params): @@ -691,15 +802,25 @@ def main(): versioning=dict(type='bool'), ceph=dict(default=False, type='bool'), encryption=dict(choices=['none', 'AES256', 'aws:kms']), - encryption_key_id=dict() + encryption_key_id=dict(), + public_access=dict(type='dict', options=dict( + block_public_acls=dict(type='bool', default=False), + ignore_public_acls=dict(type='bool', default=False), + block_public_policy=dict(type='bool', default=False), + restrict_public_buckets=dict(type='bool', default=False))), + delete_public_access=dict(type='bool', default=False) ) required_by = dict( encryption_key_id=('encryption',), ) + mutually_exclusive = [ + ['public_access', 'delete_public_access'] + ] + module = AnsibleAWSModule( - argument_spec=argument_spec, required_by=required_by + argument_spec=argument_spec, required_by=required_by, mutually_exclusive=mutually_exclusive ) region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) diff --git a/tests/integration/targets/s3_bucket/inventory b/tests/integration/targets/s3_bucket/inventory index 2968f764cfa..59a2423acdd 100644 --- a/tests/integration/targets/s3_bucket/inventory +++ b/tests/integration/targets/s3_bucket/inventory @@ -6,6 +6,7 @@ dotted tags encryption_kms encryption_sse +public_access [all:vars] ansible_connection=local diff --git a/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/public_access.yml b/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/public_access.yml new file mode 100644 index 00000000000..f7bc19846e5 --- /dev/null +++ b/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/public_access.yml @@ -0,0 +1,114 @@ +--- +- 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 bucket with public access block configuration' + s3_bucket: + name: '{{ bucket_name }}' + state: present + public_access: + block_public_acls: true + block_public_policy: true + ignore_public_acls: true + restrict_public_buckets: true + register: output + + - assert: + that: + - output.changed + - output.public_access_block + - output.public_access_block.BlockPublicAcls + - output.public_access_block.BlockPublicPolicy + - output.public_access_block.IgnorePublicAcls + - output.public_access_block.RestrictPublicBuckets + + - name: 'Re-configure public access block configuration' + s3_bucket: + name: '{{ bucket_name }}' + state: present + public_access: + block_public_acls: true + block_public_policy: false + ignore_public_acls: true + restrict_public_buckets: false + register: output + + - assert: + that: + - output.changed + - output.public_access_block + - output.public_access_block.BlockPublicAcls + - not output.public_access_block.BlockPublicPolicy + - output.public_access_block.IgnorePublicAcls + - not output.public_access_block.RestrictPublicBuckets + + - name: 'Re-configure public access block configuration (idempotency)' + s3_bucket: + name: '{{ bucket_name }}' + state: present + public_access: + block_public_acls: true + block_public_policy: false + ignore_public_acls: true + restrict_public_buckets: false + register: output + + - assert: + that: + - output is not changed + - output.public_access_block + - output.public_access_block.BlockPublicAcls + - not output.public_access_block.BlockPublicPolicy + - output.public_access_block.IgnorePublicAcls + - not output.public_access_block.RestrictPublicBuckets + + - name: 'Delete public access block configuration' + s3_bucket: + name: '{{ bucket_name }}' + state: present + delete_public_access: true + register: output + + - assert: + that: + - output is changed + - not output.public_access_block|bool + + - name: 'Delete public access block configuration (idempotency)' + s3_bucket: + name: '{{ bucket_name }}' + state: present + delete_public_access: true + register: output + + - assert: + that: + - output is not changed + - not output.public_access_block|bool + + # ============================================================ + + - name: Delete testing s3 bucket + s3_bucket: + name: '{{ bucket_name }}' + state: absent + register: output + + - assert: + that: + - output.changed + + # ============================================================ + always: + - name: Ensure all buckets are deleted + s3_bucket: + name: '{{ bucket_name }}' + state: absent + ignore_errors: yes diff --git a/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml b/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml index 3c39c5b4cb4..5b445bd5ee1 100644 --- a/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml +++ b/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml @@ -16,6 +16,7 @@ - output is changed - output.name == '{{ bucket_name }}' - not output.requester_pays + - output.public_access is undefined # ============================================================ - name: 'Try to update the simple bucket with the same values' @@ -44,7 +45,7 @@ - output is changed # ============================================================ - - name: 'Re-delete the simple s3_bucket (idemoptency)' + - name: 'Re-delete the simple s3_bucket (idempotency)' s3_bucket: name: '{{ bucket_name }}' state: absent