Skip to content

Commit

Permalink
Provide support for AWS S3 Public Access Blocking (#171)
Browse files Browse the repository at this point in the history
* Provide support for AWS S3 Public Access Blocking
* Documentation
* Execute get_bucket_public_access only if required
* changelog
* Add missing version_added entries to doc

Resolves issue #144
  • Loading branch information
zeten30 authored Nov 16, 2020
1 parent 615577b commit ebde655
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 3 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/171-s3_bucket-public_access.yml
Original file line number Diff line number Diff line change
@@ -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).
125 changes: 123 additions & 2 deletions plugins/modules/s3_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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 = {}

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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
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 @@ -6,6 +6,7 @@ dotted
tags
encryption_kms
encryption_sse
public_access

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

0 comments on commit ebde655

Please sign in to comment.