Skip to content

Commit bfb458f

Browse files
committed
s3_logging add bucket policy as default access method to target bucket
1 parent 9c66d1e commit bfb458f

File tree

4 files changed

+184
-13
lines changed

4 files changed

+184
-13
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
breaking_changes:
2+
- s3_logging - target bucket access method is changed from ACL to bucket policy. If plays require the old behavior, the action will need to specify ``target_access: acl`` (https://github.com/ansible-collections/community.aws/pull/2108)

plugins/modules/s3_logging.py

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
- "The prefix that should be prepended to the generated log files written to the target_bucket."
3535
default: ""
3636
type: str
37+
target_access:
38+
description:
39+
- "Permissions type for log delivery"
40+
default: policy
41+
choices: [ 'policy', 'acl' ]
42+
type: str
3743
extends_documentation_fragment:
3844
- amazon.aws.common.modules
3945
- amazon.aws.region.modules
@@ -58,6 +64,8 @@
5864
state: absent
5965
"""
6066

67+
import json
68+
6169
try:
6270
import botocore
6371
except ImportError:
@@ -85,8 +93,16 @@ def compare_bucket_logging(bucket_logging, target_bucket, target_prefix):
8593
return False
8694

8795

88-
def verify_acls(connection, module, target_bucket):
96+
def verify_acls(connection, module, bucket_name, target_access, target_bucket):
97+
current_obj_ownership = "BucketOwnerEnforced"
8998
try:
99+
current_ownership = connection.get_bucket_ownership_controls(aws_retry=True, Bucket=target_bucket)
100+
rules = current_ownership["OwnershipControls"]["Rules"]
101+
for rule in rules:
102+
if "ObjectOwnership" in rule:
103+
current_obj_ownership = rule["ObjectOwnership"]
104+
if target_access == "acl" and current_obj_ownership == "BucketOwnerEnforced":
105+
module.fail_json_aws(msg="The access type is set to ACL but it is disabled on target bucket")
90106
current_acl = connection.get_bucket_acl(aws_retry=True, Bucket=target_bucket)
91107
current_grants = current_acl["Grants"]
92108
except is_boto3_error_code("NoSuchBucket"):
@@ -95,25 +111,36 @@ def verify_acls(connection, module, target_bucket):
95111
botocore.exceptions.BotoCoreError,
96112
botocore.exceptions.ClientError,
97113
) as e: # pylint: disable=duplicate-except
98-
module.fail_json_aws(e, msg="Failed to fetch target bucket ACL")
114+
module.fail_json_aws(e, msg="Failed to fetch target bucket ownership")
99115

100116
required_grant = {
101117
"Grantee": {"URI": "http://acs.amazonaws.com/groups/s3/LogDelivery", "Type": "Group"},
102118
"Permission": "FULL_CONTROL",
103119
}
104120

105-
for grant in current_grants:
106-
if grant == required_grant:
121+
updated_grants = []
122+
if target_access == "acl":
123+
if required_grant not in list(current_grants):
124+
updated_grants = list(current_grants)
125+
updated_grants.append(required_grant)
126+
else:
127+
return False
128+
else:
129+
if required_grant in list(current_grants):
130+
for grant in current_grants:
131+
if grant != required_grant:
132+
updated_grants.append(grant)
133+
else:
107134
return False
108-
109-
if module.check_mode:
110-
return True
111135

112136
updated_acl = dict(current_acl)
113-
updated_grants = list(current_grants)
114-
updated_grants.append(required_grant)
115137
updated_acl["Grants"] = updated_grants
116138
del updated_acl["ResponseMetadata"]
139+
module.warn(json.dumps(updated_acl))
140+
141+
if module.check_mode:
142+
return True
143+
117144
try:
118145
connection.put_bucket_acl(aws_retry=True, Bucket=target_bucket, AccessControlPolicy=updated_acl)
119146
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
@@ -122,10 +149,70 @@ def verify_acls(connection, module, target_bucket):
122149
return True
123150

124151

125-
def enable_bucket_logging(connection, module):
152+
def verify_policy(connection, module, bucket_name, target_access, target_bucket, target_prefix):
153+
policy = {"Version": "2012-10-17", "Statement": []}
154+
policy_statement = {
155+
"Sid": bucket_name,
156+
"Effect": "Allow",
157+
"Principal": {"Service": "logging.s3.amazonaws.com"},
158+
"Action": "s3:PutObject",
159+
"Resource": f"arn:aws:s3:::{target_bucket}/{target_prefix}*",
160+
}
161+
162+
try:
163+
current_policy_raw = connection.get_bucket_policy(aws_retry=True, Bucket=target_bucket)
164+
except is_boto3_error_code("NoSuchBucket"):
165+
module.fail_json(msg=f"Target Bucket '{target_bucket}' not found")
166+
except is_boto3_error_code("NoSuchBucketPolicy"):
167+
current_policy_raw = {"Policy": json.dumps(policy)}
168+
except (
169+
botocore.exceptions.BotoCoreError,
170+
botocore.exceptions.ClientError,
171+
) as e: # pylint: disable=duplicate-except
172+
module.fail_json_aws(e, msg="Failed to fetch target bucket policy")
173+
174+
try:
175+
current_policy = json.loads(current_policy_raw["Policy"])
176+
except json.JSONDecodeError as e:
177+
module.fail_json(e, msg="Unable to parse policy")
178+
179+
new_policy_statements = []
180+
new_policy = current_policy
181+
for p in current_policy.get("Statement", []):
182+
if p == policy_statement and target_access == "policy":
183+
return False
184+
if target_access == "acl":
185+
for p in current_policy.get("Statement", []):
186+
if p != policy_statement:
187+
new_policy_statements.append(p)
188+
new_policy["Statement"] = new_policy_statements
189+
if new_policy == current_policy:
190+
return False
191+
else:
192+
new_policy = current_policy
193+
if new_policy["Statement"] is None:
194+
new_policy["Statement"] = [policy_statement]
195+
else:
196+
new_policy["Statement"].append(policy_statement)
197+
198+
if module.check_mode:
199+
return True
200+
201+
try:
202+
connection.put_bucket_policy(aws_retry=True, Bucket=target_bucket, Policy=json.dumps(new_policy))
203+
except is_boto3_error_code("MalformedPolicy"):
204+
module.fail_json(msg=f"Bad policy:\n {new_policy}")
205+
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
206+
module.fail_json_aws(e, msg="Failed to update target bucket policy to allow log delivery")
207+
208+
return True
209+
210+
211+
def enable_bucket_logging(connection, module: AnsibleAWSModule):
126212
bucket_name = module.params.get("name")
127213
target_bucket = module.params.get("target_bucket")
128214
target_prefix = module.params.get("target_prefix")
215+
target_access = module.params.get("target_access")
129216
changed = False
130217

131218
try:
@@ -139,7 +226,8 @@ def enable_bucket_logging(connection, module):
139226
module.fail_json_aws(e, msg="Failed to fetch current logging status")
140227

141228
try:
142-
changed |= verify_acls(connection, module, target_bucket)
229+
changed |= verify_acls(connection, module, bucket_name, target_access, target_bucket)
230+
changed |= verify_policy(connection, module, bucket_name, target_access, target_bucket, target_prefix)
143231

144232
if not compare_bucket_logging(bucket_logging, target_bucket, target_prefix):
145233
bucket_logging = camel_dict_to_snake_dict(bucket_logging)
@@ -196,6 +284,7 @@ def main():
196284
name=dict(required=True),
197285
target_bucket=dict(required=False, default=None),
198286
target_prefix=dict(required=False, default=""),
287+
target_access=dict(required=False, default="policy", choices=["policy", "acl"]),
199288
state=dict(required=False, default="present", choices=["present", "absent"]),
200289
)
201290

tests/integration/targets/s3_logging/defaults/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
test_bucket: '{{ tiny_prefix }}-s3-logging'
33
log_bucket_1: '{{ tiny_prefix }}-logs-1'
44
log_bucket_2: '{{ tiny_prefix }}-logs-2'
5+
log_bucket_3: '{{ tiny_prefix }}-logs-3'

tests/integration/targets/s3_logging/tasks/main.yml

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
s3_bucket:
4747
state: present
4848
name: '{{ log_bucket_1 }}'
49-
object_ownership: BucketOwnerPreferred
5049
register: output
5150
- assert:
5251
that:
@@ -57,13 +56,23 @@
5756
s3_bucket:
5857
state: present
5958
name: '{{ log_bucket_2 }}'
60-
object_ownership: BucketOwnerPreferred
6159
register: output
6260
- assert:
6361
that:
6462
- output is changed
6563
- output.name == log_bucket_2
6664

65+
- name: Create simple s3_bucket as third target for logs
66+
s3_bucket:
67+
state: present
68+
name: '{{ log_bucket_3 }}'
69+
object_ownership: BucketOwnerPreferred
70+
register: output
71+
- assert:
72+
that:
73+
- output is changed
74+
- output.name == log_bucket_3
75+
6776
# ============================================================
6877

6978
- name: Enable logging (check_mode)
@@ -152,6 +161,76 @@
152161
that:
153162
- result is not changed
154163

164+
# ============================================================
165+
166+
- name: ACL logging bucket (check_mode)
167+
s3_logging:
168+
state: present
169+
name: '{{ test_bucket }}'
170+
target_bucket: '{{ log_bucket_3 }}'
171+
target_access: acl
172+
register: result
173+
check_mode: True
174+
- assert:
175+
that:
176+
- result is changed
177+
178+
- name: ACL logging bucket
179+
s3_logging:
180+
state: present
181+
name: '{{ test_bucket }}'
182+
target_bucket: '{{ log_bucket_3 }}'
183+
target_access: acl
184+
register: result
185+
- assert:
186+
that:
187+
- result is changed
188+
189+
- name: ACL logging bucket idempotency (check_mode)
190+
s3_logging:
191+
state: present
192+
name: '{{ test_bucket }}'
193+
target_bucket: '{{ log_bucket_3 }}'
194+
target_access: acl
195+
register: result
196+
check_mode: True
197+
- assert:
198+
that:
199+
- result is not changed
200+
201+
- name: ACL logging bucket idempotency
202+
s3_logging:
203+
state: present
204+
name: '{{ test_bucket }}'
205+
target_bucket: '{{ log_bucket_3 }}'
206+
target_access: acl
207+
register: result
208+
- assert:
209+
that:
210+
- result is not changed
211+
212+
- name: ACL logging bucket convert to policy
213+
s3_logging:
214+
state: present
215+
name: '{{ test_bucket }}'
216+
target_bucket: '{{ log_bucket_3 }}'
217+
target_access: policy
218+
register: result
219+
- assert:
220+
that:
221+
- result is changed
222+
223+
- name: policy logging bucket convert to ACL
224+
s3_logging:
225+
state: present
226+
name: '{{ test_bucket }}'
227+
target_bucket: '{{ log_bucket_3 }}'
228+
target_access: acl
229+
register: result
230+
- assert:
231+
that:
232+
- result is changed
233+
155234
# ============================================================
156235

157236
- name: Change logging prefix (check_mode)

0 commit comments

Comments
 (0)