diff --git a/moto/s3/models.py b/moto/s3/models.py index de7045279..a9a2c300e 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -1598,6 +1598,9 @@ class S3Backend(BaseBackend): def get_object_acl(self, key): return key.acl + def get_object_legal_hold(self, key): + return key.lock_legal_status + def get_object_lock_configuration(self, bucket_name): bucket = self.get_bucket(bucket_name) return ( @@ -1631,7 +1634,7 @@ class S3Backend(BaseBackend): ) def put_object_lock_configuration( - self, bucket_name, lock_enabled, mode, days, years + self, bucket_name, lock_enabled, mode=None, days=None, years=None ): bucket = self.get_bucket(bucket_name) @@ -1861,7 +1864,7 @@ class S3Backend(BaseBackend): key = self.get_object(bucket_name, key_name, version_id=version_id) self.tagger.delete_all_tags_for_resource(key.arn) - def delete_object(self, bucket_name, key_name, version_id=None): + def delete_object(self, bucket_name, key_name, version_id=None, bypass=False): key_name = clean_key_name(key_name) bucket = self.get_bucket(bucket_name) @@ -1882,7 +1885,11 @@ class S3Backend(BaseBackend): for key in bucket.keys.getlist(key_name): if str(key.version_id) == str(version_id): - if hasattr(key, "is_locked") and key.is_locked: + if ( + hasattr(key, "is_locked") + and key.is_locked + and not bypass + ): raise AccessDeniedByLock if type(key) is FakeDeleteMarker: diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 6d8406667..c77e6f906 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -692,12 +692,12 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): self.backend.put_object_lock_configuration( bucket_name, - config["enabled"], - config["mode"], - config["days"], - config["years"], + config.get("enabled"), + config.get("mode"), + config.get("days"), + config.get("years"), ) - return "" + return 200, {}, "" if "versioning" in querystring: ver = re.search("([A-Za-z]+)", body.decode()) @@ -832,7 +832,10 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): bucket_name, self._acl_from_headers(request.headers) ) - if request.headers.get("x-amz-bucket-object-lock-enabled", "") == "True": + if ( + request.headers.get("x-amz-bucket-object-lock-enabled", "").lower() + == "true" + ): new_bucket.object_lock_enabled = True new_bucket.versioning_status = "Enabled" @@ -1224,7 +1227,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): bucket_name, query, key_name, headers=request.headers ) elif method == "DELETE": - return self._key_response_delete(bucket_name, query, key_name) + return self._key_response_delete(headers, bucket_name, query, key_name) elif method == "POST": return self._key_response_post(request, body, bucket_name, query, key_name) else: @@ -1312,6 +1315,10 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): tags = self.backend.get_object_tagging(key)["Tags"] template = self.response_template(S3_OBJECT_TAGGING_RESPONSE) return 200, response_headers, template.render(tags=tags) + if "legal-hold" in query: + legal_hold = self.backend.get_object_legal_hold(key) + template = self.response_template(S3_OBJECT_LEGAL_HOLD) + return 200, response_headers, template.render(legal_hold=legal_hold) response_headers.update(key.metadata) response_headers.update(key.response_dict) @@ -1567,23 +1574,27 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): return 404, response_headers, "" def _lock_config_from_xml(self, xml): + response_dict = {"enabled": False, "mode": None, "days": None, "years": None} parsed_xml = xmltodict.parse(xml) enabled = ( parsed_xml["ObjectLockConfiguration"]["ObjectLockEnabled"] == "Enabled" ) + response_dict["enabled"] = enabled - default_retention = parsed_xml["ObjectLockConfiguration"]["Rule"][ - "DefaultRetention" - ] + default_retention = parsed_xml.get("ObjectLockConfiguration").get("Rule") + if default_retention: + default_retention = default_retention.get("DefaultRetention") + mode = default_retention["Mode"] + days = int(default_retention.get("Days", 0)) + years = int(default_retention.get("Years", 0)) - mode = default_retention["Mode"] - days = int(default_retention["Days"]) if "Days" in default_retention else 0 - years = int(default_retention["Years"]) if "Years" in default_retention else 0 + if days and years: + raise MalformedXML + response_dict["mode"] = mode + response_dict["days"] = days + response_dict["years"] = years - if days and years: - raise MalformedXML - - return {"enabled": enabled, "mode": mode, "days": days, "years": years} + return response_dict def _acl_from_xml(self, xml): parsed_xml = xmltodict.parse(xml) @@ -1884,7 +1895,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): config = parsed_xml["AccelerateConfiguration"] return config["Status"] - def _key_response_delete(self, bucket_name, query, key_name): + def _key_response_delete(self, headers, bucket_name, query, key_name): self._set_action("KEY", "DELETE", query) self._authenticate_and_authorize_s3_action() @@ -1899,8 +1910,9 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): ) template = self.response_template(S3_DELETE_KEY_TAGGING_RESPONSE) return 204, {}, template.render(version_id=version_id) + bypass = headers.get("X-Amz-Bypass-Governance-Retention") success, response_meta = self.backend.delete_object( - bucket_name, key_name, version_id=version_id + bucket_name, key_name, version_id=version_id, bypass=bypass ) response_headers = {} if response_meta is not None: @@ -2312,6 +2324,12 @@ S3_OBJECT_ACL_RESPONSE = """ """ +S3_OBJECT_LEGAL_HOLD = """ + + {{ legal_hold }} + +""" + S3_OBJECT_TAGGING_RESPONSE = """\ @@ -2662,13 +2680,15 @@ S3_BUCKET_LOCK_CONFIGURATION = """ {% else %} Disabled {% endif %} + {% if mode %} - {{mode}} - {{days}} - {{years}} + {{mode}} + {{days}} + {{years}} - # + + {% endif %} """ diff --git a/tests/terraform-tests.success.txt b/tests/terraform-tests.success.txt index e50633391..a91f625c8 100644 --- a/tests/terraform-tests.success.txt +++ b/tests/terraform-tests.success.txt @@ -109,4 +109,5 @@ TestAccAWSEc2CarrierGateway TestAccDataSourceAwsNetworkInterface_ TestAccAWSNatGateway TestAccAWSRouteTable_ -TestAccAWSRouteTableAssociation_ \ No newline at end of file +TestAccAWSRouteTableAssociation_ +TestAccAWSS3Bucket_forceDestroyWithObjectLockEnabled \ No newline at end of file