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