S3: head() should return MethodNotAllowed for DeleteMarkers (#6889)
This commit is contained in:
parent
90fec47f5f
commit
3b67c9bfb0
@ -559,3 +559,10 @@ class ObjectLockConfigurationNotFoundError(S3ClientError):
|
|||||||
"ObjectLockConfigurationNotFoundError",
|
"ObjectLockConfigurationNotFoundError",
|
||||||
"Object Lock configuration does not exist for this bucket",
|
"Object Lock configuration does not exist for this bucket",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MethodNotAllowed(S3ClientError):
|
||||||
|
code = 405
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("MethodNotAllowed", "Method Not Allowed")
|
||||||
|
@ -42,6 +42,7 @@ from moto.s3.exceptions import (
|
|||||||
MissingKey,
|
MissingKey,
|
||||||
InvalidNotificationDestination,
|
InvalidNotificationDestination,
|
||||||
MalformedXML,
|
MalformedXML,
|
||||||
|
MethodNotAllowed,
|
||||||
InvalidStorageClass,
|
InvalidStorageClass,
|
||||||
InvalidTargetBucketForLogging,
|
InvalidTargetBucketForLogging,
|
||||||
CrossLocationLoggingProhibitted,
|
CrossLocationLoggingProhibitted,
|
||||||
@ -2068,6 +2069,7 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
|
|||||||
key_name: str,
|
key_name: str,
|
||||||
version_id: Optional[str] = None,
|
version_id: Optional[str] = None,
|
||||||
part_number: Optional[str] = None,
|
part_number: Optional[str] = None,
|
||||||
|
return_delete_marker: bool = False,
|
||||||
) -> Optional[FakeKey]:
|
) -> Optional[FakeKey]:
|
||||||
bucket = self.get_bucket(bucket_name)
|
bucket = self.get_bucket(bucket_name)
|
||||||
|
|
||||||
@ -2090,6 +2092,8 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
|
|||||||
key.advance()
|
key.advance()
|
||||||
return key
|
return key
|
||||||
else:
|
else:
|
||||||
|
if return_delete_marker and isinstance(key, FakeDeleteMarker):
|
||||||
|
return key # type: ignore
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def head_object(
|
def head_object(
|
||||||
@ -2099,7 +2103,12 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
|
|||||||
version_id: Optional[str] = None,
|
version_id: Optional[str] = None,
|
||||||
part_number: Optional[str] = None,
|
part_number: Optional[str] = None,
|
||||||
) -> Optional[FakeKey]:
|
) -> Optional[FakeKey]:
|
||||||
return self.get_object(bucket_name, key_name, version_id, part_number)
|
obj = self.get_object(
|
||||||
|
bucket_name, key_name, version_id, part_number, return_delete_marker=True
|
||||||
|
)
|
||||||
|
if isinstance(obj, FakeDeleteMarker):
|
||||||
|
raise MethodNotAllowed
|
||||||
|
return obj
|
||||||
|
|
||||||
def get_object_acl(self, key: FakeKey) -> Optional[FakeAcl]:
|
def get_object_acl(self, key: FakeKey) -> Optional[FakeAcl]:
|
||||||
return key.acl
|
return key.acl
|
||||||
|
@ -34,6 +34,7 @@ from .exceptions import (
|
|||||||
InvalidContentMD5,
|
InvalidContentMD5,
|
||||||
InvalidContinuationToken,
|
InvalidContinuationToken,
|
||||||
S3ClientError,
|
S3ClientError,
|
||||||
|
MethodNotAllowed,
|
||||||
MissingBucket,
|
MissingBucket,
|
||||||
MissingKey,
|
MissingKey,
|
||||||
MissingVersion,
|
MissingVersion,
|
||||||
@ -1761,9 +1762,18 @@ class S3Response(BaseResponse):
|
|||||||
if_none_match = headers.get("If-None-Match", None)
|
if_none_match = headers.get("If-None-Match", None)
|
||||||
if_unmodified_since = headers.get("If-Unmodified-Since", None)
|
if_unmodified_since = headers.get("If-Unmodified-Since", None)
|
||||||
|
|
||||||
|
try:
|
||||||
key = self.backend.head_object(
|
key = self.backend.head_object(
|
||||||
bucket_name, key_name, version_id=version_id, part_number=part_number
|
bucket_name, key_name, version_id=version_id, part_number=part_number
|
||||||
)
|
)
|
||||||
|
except MethodNotAllowed:
|
||||||
|
headers = {
|
||||||
|
"x-amz-delete-marker": "true",
|
||||||
|
"x-amz-version-id": version_id,
|
||||||
|
"allow": "DELETE",
|
||||||
|
"content-type": "application/xml",
|
||||||
|
}
|
||||||
|
return 405, headers, "Method Not Allowed"
|
||||||
if key:
|
if key:
|
||||||
response_headers.update(key.metadata)
|
response_headers.update(key.metadata)
|
||||||
response_headers.update(key.response_dict)
|
response_headers.update(key.response_dict)
|
||||||
|
@ -1684,6 +1684,9 @@ def test_delete_versioned_bucket():
|
|||||||
@s3_aws_verified
|
@s3_aws_verified
|
||||||
def test_delete_versioned_bucket_returns_metadata(name=None):
|
def test_delete_versioned_bucket_returns_metadata(name=None):
|
||||||
client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||||
|
resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
|
||||||
|
bucket = resource.Bucket(name)
|
||||||
|
versions = bucket.object_versions
|
||||||
|
|
||||||
client.put_bucket_versioning(
|
client.put_bucket_versioning(
|
||||||
Bucket=name, VersioningConfiguration={"Status": "Enabled"}
|
Bucket=name, VersioningConfiguration={"Status": "Enabled"}
|
||||||
@ -1696,12 +1699,30 @@ def test_delete_versioned_bucket_returns_metadata(name=None):
|
|||||||
|
|
||||||
# Delete the object
|
# Delete the object
|
||||||
del_file = client.delete_object(Bucket=name, Key="test1")
|
del_file = client.delete_object(Bucket=name, Key="test1")
|
||||||
|
deleted_version_id = del_file["VersionId"]
|
||||||
assert del_file["DeleteMarker"] is True
|
assert del_file["DeleteMarker"] is True
|
||||||
assert del_file["VersionId"] is not None
|
assert deleted_version_id is not None
|
||||||
|
|
||||||
# We now have one DeleteMarker
|
# We now have one DeleteMarker
|
||||||
assert len(client.list_object_versions(Bucket=name)["DeleteMarkers"]) == 1
|
assert len(client.list_object_versions(Bucket=name)["DeleteMarkers"]) == 1
|
||||||
|
|
||||||
|
# list_object_versions returns the object itself, and a DeleteMarker
|
||||||
|
# object.head() returns a 'x-amz-delete-marker' header
|
||||||
|
# delete_marker.head() returns a 405
|
||||||
|
for version in versions.filter(Prefix="test1"):
|
||||||
|
if version.version_id == deleted_version_id:
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
version.head()
|
||||||
|
err = exc.value.response
|
||||||
|
assert err["Error"] == {"Code": "405", "Message": "Method Not Allowed"}
|
||||||
|
assert err["ResponseMetadata"]["HTTPStatusCode"] == 405
|
||||||
|
assert (
|
||||||
|
err["ResponseMetadata"]["HTTPHeaders"]["x-amz-delete-marker"] == "true"
|
||||||
|
)
|
||||||
|
assert err["ResponseMetadata"]["HTTPHeaders"]["allow"] == "DELETE"
|
||||||
|
else:
|
||||||
|
assert version.head()["ResponseMetadata"]["HTTPStatusCode"] == 200
|
||||||
|
|
||||||
# Delete the same object gives a new version id
|
# Delete the same object gives a new version id
|
||||||
del_mrk1 = client.delete_object(Bucket=name, Key="test1")
|
del_mrk1 = client.delete_object(Bucket=name, Key="test1")
|
||||||
assert del_mrk1["DeleteMarker"] is True
|
assert del_mrk1["DeleteMarker"] is True
|
||||||
@ -1717,6 +1738,20 @@ def test_delete_versioned_bucket_returns_metadata(name=None):
|
|||||||
assert del_mrk2["DeleteMarker"] is True
|
assert del_mrk2["DeleteMarker"] is True
|
||||||
assert del_mrk2["VersionId"] == del_mrk1["VersionId"]
|
assert del_mrk2["VersionId"] == del_mrk1["VersionId"]
|
||||||
|
|
||||||
|
for version in versions.filter(Prefix="test1"):
|
||||||
|
if version.version_id == deleted_version_id:
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
version.head()
|
||||||
|
err = exc.value.response
|
||||||
|
assert err["Error"] == {"Code": "405", "Message": "Method Not Allowed"}
|
||||||
|
assert err["ResponseMetadata"]["HTTPStatusCode"] == 405
|
||||||
|
assert (
|
||||||
|
err["ResponseMetadata"]["HTTPHeaders"]["x-amz-delete-marker"] == "true"
|
||||||
|
)
|
||||||
|
assert err["ResponseMetadata"]["HTTPHeaders"]["allow"] == "DELETE"
|
||||||
|
else:
|
||||||
|
assert version.head()["ResponseMetadata"]["HTTPStatusCode"] == 200
|
||||||
|
|
||||||
# We now have only one DeleteMarker
|
# We now have only one DeleteMarker
|
||||||
assert len(client.list_object_versions(Bucket=name)["DeleteMarkers"]) == 1
|
assert len(client.list_object_versions(Bucket=name)["DeleteMarkers"]) == 1
|
||||||
|
|
||||||
@ -1732,6 +1767,13 @@ def test_delete_versioned_bucket_returns_metadata(name=None):
|
|||||||
assert len(client.list_object_versions(Bucket=name)["DeleteMarkers"]) == 1
|
assert len(client.list_object_versions(Bucket=name)["DeleteMarkers"]) == 1
|
||||||
assert "Versions" not in client.list_object_versions(Bucket=name)
|
assert "Versions" not in client.list_object_versions(Bucket=name)
|
||||||
|
|
||||||
|
# Because we only have DeleteMarkers, we can not call `head()` on any of othem
|
||||||
|
for version in versions.filter(Prefix="test1"):
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
version.head()
|
||||||
|
err = exc.value.response
|
||||||
|
assert err["Error"] == {"Code": "405", "Message": "Method Not Allowed"}
|
||||||
|
|
||||||
# Delete the last marker
|
# Delete the last marker
|
||||||
del_mrk4 = client.delete_object(
|
del_mrk4 = client.delete_object(
|
||||||
Bucket=name, Key="test1", VersionId=del_mrk2["VersionId"]
|
Bucket=name, Key="test1", VersionId=del_mrk2["VersionId"]
|
||||||
|
Loading…
Reference in New Issue
Block a user