Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide support for AWS S3 Public Access Blocking #171

Merged
merged 9 commits into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 121 additions & 2 deletions plugins/modules/s3_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,35 @@
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
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

extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2
Expand Down Expand Up @@ -153,6 +182,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 +222,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 +235,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 +405,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 +569,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 +678,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 +800,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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add a test to the 'basic' suite to make sure your PR doesn't change the default behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I get this. Can you please explain in further detail? I don't see any file named basic in tests/. I've followed what's already in place for 'encryption_kms' test. It's in inventory and in tasks/. Thanks in advance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplest version would be to tweak https://github.com/ansible-collections/amazon.aws/blob/main/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml and make sure that the returned values for public_access match those of Amazon's defaults if you didn't pass public_access.

The intention is to ensure that in adding the new public_access option you've not changed the default behaviour when someone creates a bucket.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes perfect sense now:) TY.

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