diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
index 3ed385f1c..7ea21b096 100644
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -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 %}{{ bucket }}{% endblock %}
@@ -10,6 +10,10 @@ ERROR_WITH_KEY_NAME = """{% extends 'single_error' %}
{% block extra %}{{ key_name }}{% endblock %}
"""
+ERROR_WITH_CONDITION_NAME = """{% extends 'single_error' %}
+{% block extra %}{{ 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
+ )
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index 395cb5736..fa3e536a7 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -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, ""
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index 960594801..4139cf055 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -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():