Skip to content

Commit

Permalink
add object ownership controls options for s3 bucket (#311)
Browse files Browse the repository at this point in the history
add object ownership controls options for s3 bucket

Reviewed-by: https://github.com/apps/ansible-zuul
  • Loading branch information
abikouo authored May 4, 2021
1 parent 9e2cc4a commit 0fc4761
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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)
99 changes: 96 additions & 3 deletions plugins/modules/s3_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -809,15 +893,18 @@ 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(
encryption_key_id=('encryption',),
)

mutually_exclusive = [
['public_access', 'delete_public_access']
['public_access', 'delete_public_access'],
['delete_object_ownership', 'object_ownership']
]

module = AnsibleAWSModule(
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions tests/integration/targets/s3_bucket/inventory
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ tags
encryption_kms
encryption_sse
public_access
ownership_controls

[all:vars]
ansible_connection=local
Expand Down
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 0fc4761

Please sign in to comment.