Skip to content

Commit caaa494

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

File tree

3 files changed

+186
-13
lines changed

3 files changed

+186
-13
lines changed

plugins/modules/s3_logging.py

+104-11
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,7 @@
5864
state: absent
5965
"""
6066

67+
import json
6168
try:
6269
import botocore
6370
except ImportError:
@@ -85,8 +92,16 @@ def compare_bucket_logging(bucket_logging, target_bucket, target_prefix):
8592
return False
8693

8794

88-
def verify_acls(connection, module, target_bucket):
95+
def verify_acls(connection, module, bucket_name, target_access, target_bucket):
96+
current_obj_ownership = "BucketOwnerEnforced"
8997
try:
98+
current_ownership = connection.get_bucket_ownership_controls(aws_retry=True, Bucket=target_bucket)
99+
rules = current_ownership["OwnershipControls"]["Rules"]
100+
for rule in rules:
101+
if "ObjectOwnership" in rule:
102+
current_obj_ownership = rule["ObjectOwnership"]
103+
if target_access == "acl" and current_obj_ownership == "BucketOwnerEnforced":
104+
module.fail_json_aws(msg="The access type is set to ACL but it is disabled on target bucket")
90105
current_acl = connection.get_bucket_acl(aws_retry=True, Bucket=target_bucket)
91106
current_grants = current_acl["Grants"]
92107
except is_boto3_error_code("NoSuchBucket"):
@@ -95,25 +110,36 @@ def verify_acls(connection, module, target_bucket):
95110
botocore.exceptions.BotoCoreError,
96111
botocore.exceptions.ClientError,
97112
) as e: # pylint: disable=duplicate-except
98-
module.fail_json_aws(e, msg="Failed to fetch target bucket ACL")
113+
module.fail_json_aws(e, msg="Failed to fetch target bucket ownership")
99114

100115
required_grant = {
101116
"Grantee": {"URI": "http://acs.amazonaws.com/groups/s3/LogDelivery", "Type": "Group"},
102117
"Permission": "FULL_CONTROL",
103118
}
104119

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

112135
updated_acl = dict(current_acl)
113-
updated_grants = list(current_grants)
114-
updated_grants.append(required_grant)
115136
updated_acl["Grants"] = updated_grants
116137
del updated_acl["ResponseMetadata"]
138+
module.warn(json.dumps(updated_acl))
139+
140+
if module.check_mode:
141+
return True
142+
117143
try:
118144
connection.put_bucket_acl(aws_retry=True, Bucket=target_bucket, AccessControlPolicy=updated_acl)
119145
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
@@ -122,10 +148,75 @@ def verify_acls(connection, module, target_bucket):
122148
return True
123149

124150

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

131222
try:
@@ -139,7 +230,8 @@ def enable_bucket_logging(connection, module):
139230
module.fail_json_aws(e, msg="Failed to fetch current logging status")
140231

141232
try:
142-
changed |= verify_acls(connection, module, target_bucket)
233+
changed |= verify_acls(connection, module, bucket_name, target_access, target_bucket)
234+
changed |= verify_policy(connection, module, bucket_name, target_access, target_bucket, target_prefix)
143235

144236
if not compare_bucket_logging(bucket_logging, target_bucket, target_prefix):
145237
bucket_logging = camel_dict_to_snake_dict(bucket_logging)
@@ -196,6 +288,7 @@ def main():
196288
name=dict(required=True),
197289
target_bucket=dict(required=False, default=None),
198290
target_prefix=dict(required=False, default=""),
291+
target_access=dict(required=False, default="policy", choices=["policy", "acl"]),
199292
state=dict(required=False, default="present", choices=["present", "absent"]),
200293
)
201294

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

+1
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

+81-2
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)