Add If-Match, If-None-Match and If-Unmodified-Since to S3 GET/HEAD (#3021)

fixes #2705
This commit is contained in:
Arcadiy Ivanov 2020-09-11 05:17:39 -04:00 committed by GitHub
parent 7054143701
commit c2d1ce2c14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 167 additions and 7 deletions

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals
from moto.core.exceptions import RESTError
from moto.core.exceptions import RESTError
ERROR_WITH_BUCKET_NAME = """{% extends 'single_error' %}
{% block extra %}<BucketName>{{ bucket }}</BucketName>{% endblock %}
@ -10,6 +10,10 @@ ERROR_WITH_KEY_NAME = """{% extends 'single_error' %}
{% block extra %}<KeyName>{{ key_name }}</KeyName>{% endblock %}
"""
ERROR_WITH_CONDITION_NAME = """{% extends 'single_error' %}
{% block extra %}<Condition>{{ condition }}</Condition>{% endblock %}
"""
class S3ClientError(RESTError):
def __init__(self, *args, **kwargs):
@ -386,3 +390,17 @@ class NoSuchUpload(S3ClientError):
super(NoSuchUpload, self).__init__(
"NoSuchUpload", "The specified multipart upload does not exist."
)
class PreconditionFailed(S3ClientError):
code = 412
def __init__(self, failed_condition, **kwargs):
kwargs.setdefault("template", "condition_error")
self.templates["condition_error"] = ERROR_WITH_CONDITION_NAME
super(PreconditionFailed, self).__init__(
"PreconditionFailed",
"At least one of the pre-conditions you specified did not hold",
condition=failed_condition,
**kwargs
)

View File

@ -36,6 +36,7 @@ from .exceptions import (
InvalidNotificationEvent,
ObjectNotInActiveTierError,
NoSystemTags,
PreconditionFailed,
)
from .models import (
s3_backend,
@ -1149,13 +1150,28 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
)
version_id = query.get("versionId", [None])[0]
if_modified_since = headers.get("If-Modified-Since", None)
if_match = headers.get("If-Match", None)
if_none_match = headers.get("If-None-Match", None)
if_unmodified_since = headers.get("If-Unmodified-Since", None)
key = self.backend.get_object(bucket_name, key_name, version_id=version_id)
if key is None:
raise MissingKey(key_name)
if if_unmodified_since:
if_unmodified_since = str_to_rfc_1123_datetime(if_unmodified_since)
if key.last_modified > if_unmodified_since:
raise PreconditionFailed("If-Unmodified-Since")
if if_match and key.etag != if_match:
raise PreconditionFailed("If-Match")
if if_modified_since:
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
if if_modified_since and key.last_modified < if_modified_since:
if key.last_modified < if_modified_since:
return 304, response_headers, "Not Modified"
if if_none_match and key.etag == if_none_match:
return 304, response_headers, "Not Modified"
if "acl" in query:
template = self.response_template(S3_OBJECT_ACL_RESPONSE)
return 200, response_headers, template.render(obj=key)
@ -1319,8 +1335,9 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
part_number = int(part_number)
if_modified_since = headers.get("If-Modified-Since", None)
if if_modified_since:
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
if_match = headers.get("If-Match", None)
if_none_match = headers.get("If-None-Match", None)
if_unmodified_since = headers.get("If-Unmodified-Since", None)
key = self.backend.get_object(
bucket_name, key_name, version_id=version_id, part_number=part_number
@ -1329,10 +1346,21 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
response_headers.update(key.metadata)
response_headers.update(key.response_dict)
if if_modified_since and key.last_modified < if_modified_since:
if if_unmodified_since:
if_unmodified_since = str_to_rfc_1123_datetime(if_unmodified_since)
if key.last_modified > if_unmodified_since:
return 412, response_headers, ""
if if_match and key.etag != if_match:
return 412, response_headers, ""
if if_modified_since:
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
if key.last_modified < if_modified_since:
return 304, response_headers, "Not Modified"
if if_none_match and key.etag == if_none_match:
return 304, response_headers, "Not Modified"
else:
return 200, response_headers, ""
return 200, response_headers, ""
else:
return 404, response_headers, ""

View File

@ -2335,6 +2335,64 @@ def test_boto3_get_object_if_modified_since():
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
@mock_s3
def test_boto3_get_object_if_unmodified_since():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
bucket_name = "blah"
s3.create_bucket(Bucket=bucket_name)
key = "hello.txt"
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
with assert_raises(botocore.exceptions.ClientError) as err:
s3.get_object(
Bucket=bucket_name,
Key=key,
IfUnmodifiedSince=datetime.datetime.utcnow() - datetime.timedelta(hours=1),
)
e = err.exception
e.response["Error"]["Code"].should.equal("PreconditionFailed")
e.response["Error"]["Condition"].should.equal("If-Unmodified-Since")
@mock_s3
def test_boto3_get_object_if_match():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
bucket_name = "blah"
s3.create_bucket(Bucket=bucket_name)
key = "hello.txt"
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
with assert_raises(botocore.exceptions.ClientError) as err:
s3.get_object(
Bucket=bucket_name, Key=key, IfMatch='"hello"',
)
e = err.exception
e.response["Error"]["Code"].should.equal("PreconditionFailed")
e.response["Error"]["Condition"].should.equal("If-Match")
@mock_s3
def test_boto3_get_object_if_none_match():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
bucket_name = "blah"
s3.create_bucket(Bucket=bucket_name)
key = "hello.txt"
etag = s3.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
with assert_raises(botocore.exceptions.ClientError) as err:
s3.get_object(
Bucket=bucket_name, Key=key, IfNoneMatch=etag,
)
e = err.exception
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
@mock_s3
def test_boto3_head_object_if_modified_since():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
@ -2355,6 +2413,62 @@ def test_boto3_head_object_if_modified_since():
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
@mock_s3
def test_boto3_head_object_if_unmodified_since():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
bucket_name = "blah"
s3.create_bucket(Bucket=bucket_name)
key = "hello.txt"
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
with assert_raises(botocore.exceptions.ClientError) as err:
s3.head_object(
Bucket=bucket_name,
Key=key,
IfUnmodifiedSince=datetime.datetime.utcnow() - datetime.timedelta(hours=1),
)
e = err.exception
e.response["Error"].should.equal({"Code": "412", "Message": "Precondition Failed"})
@mock_s3
def test_boto3_head_object_if_match():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
bucket_name = "blah"
s3.create_bucket(Bucket=bucket_name)
key = "hello.txt"
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
with assert_raises(botocore.exceptions.ClientError) as err:
s3.head_object(
Bucket=bucket_name, Key=key, IfMatch='"hello"',
)
e = err.exception
e.response["Error"].should.equal({"Code": "412", "Message": "Precondition Failed"})
@mock_s3
def test_boto3_head_object_if_none_match():
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
bucket_name = "blah"
s3.create_bucket(Bucket=bucket_name)
key = "hello.txt"
etag = s3.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
with assert_raises(botocore.exceptions.ClientError) as err:
s3.head_object(
Bucket=bucket_name, Key=key, IfNoneMatch=etag,
)
e = err.exception
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
@mock_s3
@reduced_min_part_size
def test_boto3_multipart_etag():