From 0fc476138913c4eec81fb63ef5b4fda8b1f52883 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Tue, 4 May 2021 23:28:59 +0200 Subject: [PATCH] add object ownership controls options for s3 bucket (#311) add object ownership controls options for s3 bucket Reviewed-by: https://github.com/apps/ansible-zuul --- ...-allow-object-ownership-configuration.yaml | 2 + plugins/modules/s3_bucket.py | 99 ++++++++++++++- tests/integration/targets/s3_bucket/inventory | 1 + .../s3_bucket/tasks/ownership_controls.yml | 119 ++++++++++++++++++ 4 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/311-s3_bucket-allow-object-ownership-configuration.yaml create mode 100644 tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/ownership_controls.yml diff --git a/changelogs/fragments/311-s3_bucket-allow-object-ownership-configuration.yaml b/changelogs/fragments/311-s3_bucket-allow-object-ownership-configuration.yaml new file mode 100644 index 00000000000..d822857b1d9 --- /dev/null +++ b/changelogs/fragments/311-s3_bucket-allow-object-ownership-configuration.yaml @@ -0,0 +1,2 @@ +minor_changes: + - s3_bucket - add new option ``object_ownership`` to configure object ownership (https://github.com/ansible-collections/amazon.aws/pull/311) diff --git a/plugins/modules/s3_bucket.py b/plugins/modules/s3_bucket.py index 512f0923fd4..0ab2de88f78 100644 --- a/plugins/modules/s3_bucket.py +++ b/plugins/modules/s3_bucket.py @@ -25,7 +25,9 @@ description: - Manage S3 buckets in AWS, DigitalOcean, Ceph, Walrus, FakeS3 and StorageGRID. requirements: [ boto3 ] -author: "Rob White (@wimnat)" +author: + - Rob White (@wimnat) + - Aubin Bikouo (@abikouo) options: force: description: @@ -120,6 +122,24 @@ default: false type: bool version_added: 1.3.0 + object_ownership: + description: + - Allow bucket's ownership controls. + - C(BucketOwnerPreferred) - Objects uploaded to the bucket change ownership to the bucket owner + if the objects are uploaded with the bucket-owner-full-control canned ACL. + - C(ObjectWriter) - The uploading account will own the object + if the object is uploaded with the bucket-owner-full-control canned ACL. + - This option cannot be used together with a I(delete_object_ownership) definition. + choices: [ 'BucketOwnerPreferred', 'ObjectWriter' ] + type: str + version_added: 2.0.0 + delete_object_ownership: + description: + - Delete bucket's ownership controls. + - This option cannot be used together with a I(object_ownership) definition. + default: false + type: bool + version_added: 2.0.0 extends_documentation_fragment: - amazon.aws.aws @@ -202,6 +222,18 @@ name: mys3bucket state: present delete_public_access: true + +# Create a bucket with object ownership controls set to ObjectWriter +- amazon.aws.s3_bucket: + name: mys3bucket + state: present + object_ownership: ObjectWriter + +# Delete onwership controls from bucket +- amazon.aws.s3_bucket: + name: mys3bucket + state: present + delete_object_ownership: true ''' import json @@ -240,6 +272,8 @@ def create_or_update_bucket(s3_client, module, location): 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") + delete_object_ownership = module.params.get("delete_object_ownership") + object_ownership = module.params.get("object_ownership") changed = False result = {} @@ -440,6 +474,23 @@ def create_or_update_bucket(s3_client, module, location): changed = True result['public_access_block'] = {} + # -- Bucket ownership + bucket_ownership = get_bucket_ownership_cntrl(s3_client, module, name) + result['object_ownership'] = bucket_ownership + if delete_object_ownership or object_ownership is not None: + if delete_object_ownership: + # delete S3 buckect ownership + if bucket_ownership is not None: + delete_bucket_ownership(s3_client, name) + changed = True + result['object_ownership'] = None + else: + # update S3 bucket ownership + if bucket_ownership != object_ownership: + put_bucket_ownership(s3_client, name, object_ownership) + changed = True + result['object_ownership'] = object_ownership + # Module exit module.exit_json(changed=changed, name=name, **result) @@ -588,6 +639,26 @@ def delete_bucket_public_access(s3_client, bucket_name): s3_client.delete_public_access_block(Bucket=bucket_name) +@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) +def delete_bucket_ownership(s3_client, bucket_name): + ''' + Delete bucket ownership controls from S3 bucket + ''' + s3_client.delete_bucket_ownership_controls(Bucket=bucket_name) + + +@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) +def put_bucket_ownership(s3_client, bucket_name, target): + ''' + Put bucket ownership controls for S3 bucket + ''' + s3_client.put_bucket_ownership_controls( + Bucket=bucket_name, + OwnershipControls={ + 'Rules': [{'ObjectOwnership': target}] + }) + + def wait_policy_is_applied(module, s3_client, bucket_name, expected_policy, should_fail=True): for dummy in range(0, 12): try: @@ -692,6 +763,19 @@ def get_bucket_public_access(s3_client, bucket_name): return {} +def get_bucket_ownership_cntrl(s3_client, module, bucket_name): + ''' + Get current bucket public access block + ''' + if not module.botocore_at_least('1.8.11'): + return None + try: + bucket_ownership = s3_client.get_bucket_ownership_controls(Bucket=bucket_name) + return bucket_ownership['OwnershipControls']['Rules'][0]['ObjectOwnership'] + except is_boto3_error_code(['OwnershipControlsNotFoundError', 'NoSuchOwnershipControls']): + return None + + def paginated_list(s3_client, **pagination_params): pg = s3_client.get_paginator('list_objects_v2') for page in pg.paginate(**pagination_params): @@ -809,7 +893,9 @@ def main(): 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) + delete_public_access=dict(type='bool', default=False), + object_ownership=dict(type='str', choices=['BucketOwnerPreferred', 'ObjectWriter']), + delete_object_ownership=dict(type='bool', default=False), ) required_by = dict( @@ -817,7 +903,8 @@ def main(): ) mutually_exclusive = [ - ['public_access', 'delete_public_access'] + ['public_access', 'delete_public_access'], + ['delete_object_ownership', 'object_ownership'] ] module = AnsibleAWSModule( @@ -857,6 +944,12 @@ def main(): state = module.params.get("state") encryption = module.params.get("encryption") encryption_key_id = module.params.get("encryption_key_id") + delete_object_ownership = module.params.get('delete_object_ownership') + object_ownership = module.params.get('object_ownership') + + if delete_object_ownership is not None or object_ownership is not None: + if not module.botocore_at_least('1.8.11'): + module.fail_json(msg="Managing bucket ownership controls requires botocore version >= 1.8.11") if not hasattr(s3_client, "get_bucket_encryption"): if encryption is not None: diff --git a/tests/integration/targets/s3_bucket/inventory b/tests/integration/targets/s3_bucket/inventory index 59a2423acdd..62dcff0fbe7 100644 --- a/tests/integration/targets/s3_bucket/inventory +++ b/tests/integration/targets/s3_bucket/inventory @@ -7,6 +7,7 @@ tags encryption_kms encryption_sse public_access +ownership_controls [all:vars] ansible_connection=local diff --git a/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/ownership_controls.yml b/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/ownership_controls.yml new file mode 100644 index 00000000000..c651aac861e --- /dev/null +++ b/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/ownership_controls.yml @@ -0,0 +1,119 @@ +--- +- 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: + + # ============================================================ + - set_fact: + local_bucket_name: "{{ bucket_name | hash('md5')}}ownership" + + - name: 'Create a simple bucket bad value for ownership controls' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + object_ownership: default + ignore_errors: true + register: output + + - assert: + that: + - output.failed + + - name: 'Create bucket with object_ownership set to object_writer' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + ignore_errors: true + register: output + + - assert: + that: + - output.changed + - not output.object_ownership|bool + + - name: delete s3 bucket + s3_bucket: + name: '{{ local_bucket_name }}' + state: absent + + - name: 'create s3 bucket with object ownership controls' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + object_ownership: ObjectWriter + register: output + + - assert: + that: + - output.changed + - output.object_ownership + - output.object_ownership == 'ObjectWriter' + + - name: 'update s3 bucket ownership controls' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + object_ownership: BucketOwnerPreferred + register: output + + - assert: + that: + - output.changed + - output.object_ownership + - output.object_ownership == 'BucketOwnerPreferred' + + - name: 'test idempotency update s3 bucket ownership controls' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + object_ownership: BucketOwnerPreferred + register: output + + - assert: + that: + - output.changed is false + - output.object_ownership + - output.object_ownership == 'BucketOwnerPreferred' + + - name: 'delete s3 bucket ownership controls' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + delete_object_ownership: true + register: output + + - assert: + that: + - output.changed + - not output.object_ownership|bool + + - name: 'delete s3 bucket ownership controls once again (idempotency)' + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + delete_object_ownership: true + register: idempotency + + - assert: + that: + - not idempotency.changed + - not idempotency.object_ownership|bool + + # ============================================================ + always: + - name: delete s3 bucket ownership controls + s3_bucket: + name: '{{ local_bucket_name }}' + state: present + delete_object_ownership: true + ignore_errors: yes + + - name: Ensure all buckets are deleted + s3_bucket: + name: '{{ local_bucket_name }}' + state: absent + ignore_errors: yes