Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 44992c0

Browse files
authored
S3: fix exception from ObjectLock retention (#12165)
1 parent 9ae3666 commit 44992c0

File tree

4 files changed

+49
-44
lines changed

4 files changed

+49
-44
lines changed

‎localstack-core/localstack/services/s3/provider.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from operator import itemgetter
1010
from typing import IO, Optional, Union
1111
from urllib import parse as urlparse
12+
from zoneinfo import ZoneInfo
1213

1314
from localstack import config
1415
from localstack.aws.api import CommonServiceException, RequestContext, handler
@@ -2011,7 +2012,7 @@ def restore_object(
20112012
return RestoreObjectOutput()
20122013

20132014
restore_expiration_date = add_expiration_days_to_datetime(
2014-
datetime.datetime.utcnow(), restore_days
2015+
datetime.datetime.now(datetime.UTC), restore_days
20152016
)
20162017
# TODO: add a way to transition from ongoing-request=true to false? for now it is instant
20172018
s3_object.restore = f'ongoing-request="false", expiry-date="{restore_expiration_date}"'
@@ -3552,6 +3553,16 @@ def put_object_retention(
35523553
):
35533554
raise MalformedXML()
35543555

3556+
if retention and retention["RetainUntilDate"] < datetime.datetime.now(datetime.UTC):
3557+
# weirdly, this date is format as following: Tue Dec 31 16:00:00 PST 2019
3558+
# it contains the timezone as PST, even if you target a bucket in Europe or Asia
3559+
pst_datetime = retention["RetainUntilDate"].astimezone(tz=ZoneInfo("US/Pacific"))
3560+
raise InvalidArgument(
3561+
"The retain until date must be in the future!",
3562+
ArgumentName="RetainUntilDate",
3563+
ArgumentValue=pst_datetime.strftime("%a %b %d %H:%M:%S %Z %Y"),
3564+
)
3565+
35553566
if (
35563567
not retention
35573568
or (s3_object.lock_until and s3_object.lock_until > retention["RetainUntilDate"])

‎tests/aws/services/s3/test_s3.py

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3606,12 +3606,8 @@ def test_delete_non_existing_keys(self, s3_bucket, snapshot, aws_client):
36063606

36073607
@markers.aws.validated
36083608
@markers.snapshot.skip_snapshot_verify(
3609-
path=[
3610-
"$..Deleted..VersionId", # we cannot guarantee order nor we can sort it
3611-
"$..Delimiter",
3612-
"$..EncodingType",
3613-
"$..VersionIdMarker",
3614-
]
3609+
# we cannot guarantee order nor we can sort it
3610+
path=["$..Deleted..VersionId"],
36153611
)
36163612
def test_delete_keys_in_versioned_bucket(self, s3_bucket, snapshot, aws_client):
36173613
# see https://docs.aws.amazon.com/AmazonS3/latest/userguide/DeletingObjectVersions.html
@@ -4434,9 +4430,6 @@ def test_upload_big_file(self, s3_create_bucket, snapshot, aws_client):
44344430
snapshot.match("head_object_key2", rs)
44354431

44364432
@markers.aws.validated
4437-
@markers.snapshot.skip_snapshot_verify(
4438-
paths=["$..Delimiter", "$..EncodingType", "$..VersionIdMarker"]
4439-
)
44404433
def test_get_bucket_versioning_order(self, s3_bucket, snapshot, aws_client):
44414434
snapshot.add_transformer(snapshot.transform.s3_api())
44424435
rs = aws_client.s3.list_object_versions(Bucket=s3_bucket, EncodingType="url")
@@ -4478,9 +4471,6 @@ def test_etag_on_get_object_call(self, s3_bucket, snapshot, aws_client):
44784471
snapshot.match("get_object_range", rs)
44794472

44804473
@markers.aws.validated
4481-
@markers.snapshot.skip_snapshot_verify(
4482-
paths=["$..Delimiter", "$..EncodingType", "$..VersionIdMarker"]
4483-
)
44844474
def test_s3_delete_object_with_version_id(self, s3_bucket, snapshot, aws_client):
44854475
snapshot.add_transformer(snapshot.transform.s3_api())
44864476

@@ -4527,9 +4517,6 @@ def test_s3_delete_object_with_version_id(self, s3_bucket, snapshot, aws_client)
45274517
snapshot.match("get_bucket_versioning_suspended", rs)
45284518

45294519
@markers.aws.validated
4530-
@markers.snapshot.skip_snapshot_verify(
4531-
paths=["$..Delimiter", "$..EncodingType", "$..VersionIdMarker"]
4532-
)
45334520
def test_s3_put_object_versioned(self, s3_bucket, snapshot, aws_client):
45344521
snapshot.add_transformer(snapshot.transform.s3_api())
45354522

@@ -4661,9 +4648,6 @@ def test_s3_batch_delete_objects_using_requests_with_acl(
46614648
snapshot.match("list-remaining-objects", response)
46624649

46634650
@markers.aws.validated
4664-
@markers.snapshot.skip_snapshot_verify(
4665-
paths=["$..DeleteResult.Deleted..VersionId", "$..Prefix", "$..DeleteResult.@xmlns"]
4666-
)
46674651
def test_s3_batch_delete_public_objects_using_requests(
46684652
self, s3_bucket, allow_bucket_acl, snapshot, aws_client, anonymous_client
46694653
):
@@ -4713,11 +4697,6 @@ def test_s3_batch_delete_public_objects_using_requests(
47134697
snapshot.match("list-remaining-objects", response)
47144698

47154699
@markers.aws.validated
4716-
@markers.snapshot.skip_snapshot_verify(
4717-
paths=[
4718-
"$..Prefix",
4719-
]
4720-
)
47214700
def test_s3_batch_delete_objects(self, s3_bucket, snapshot, aws_client):
47224701
snapshot.add_transformer(snapshot.transform.s3_api())
47234702
snapshot.add_transformer(snapshot.transform.key_value("Key"))
@@ -6179,7 +6158,6 @@ def _put_bucket_inventory_configuration(config_id: str):
61796158
"use_virtual_address",
61806159
[True, False],
61816160
)
6182-
@markers.snapshot.skip_snapshot_verify(paths=["$..x-amz-server-side-encryption"])
61836161
@markers.aws.validated
61846162
def test_get_object_content_length_with_virtual_host(
61856163
self,
@@ -9375,25 +9353,19 @@ def test_lifecycle_expired_object_delete_marker(self, s3_bucket, snapshot, aws_c
93759353

93769354
class TestS3ObjectLockRetention:
93779355
@markers.aws.validated
9378-
@markers.snapshot.skip_snapshot_verify(
9379-
paths=[
9380-
# TODO: fix the exception for update-retention-no-bypass
9381-
"$.update-retention-no-bypass..ArgumentName",
9382-
"$.update-retention-no-bypass..ArgumentValue",
9383-
"$.update-retention-no-bypass..Code",
9384-
"$.update-retention-no-bypass..HTTPStatusCode",
9385-
"$.update-retention-no-bypass..Message",
9386-
]
9387-
)
93889356
def test_s3_object_retention_exc(self, aws_client, s3_create_bucket, snapshot):
93899357
snapshot.add_transformer(snapshot.transform.key_value("BucketName"))
93909358
s3_bucket_locked = s3_create_bucket(ObjectLockEnabledForBucket=True)
9359+
9360+
current_year = datetime.datetime.now().year
9361+
future_datetime = datetime.datetime(current_year + 5, 1, 1)
9362+
93919363
# non-existing bucket
93929364
with pytest.raises(ClientError) as e:
93939365
aws_client.s3.put_object_retention(
93949366
Bucket=f"non-existing-bucket-{long_uid()}",
93959367
Key="fake-key",
9396-
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": datetime.datetime(2030, 1, 1)},
9368+
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": future_datetime},
93979369
)
93989370
snapshot.match("put-object-retention-no-bucket", e.value.response)
93999371

@@ -9402,7 +9374,7 @@ def test_s3_object_retention_exc(self, aws_client, s3_create_bucket, snapshot):
94029374
aws_client.s3.put_object_retention(
94039375
Bucket=s3_bucket_locked,
94049376
Key="non-existing-key",
9405-
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": datetime.datetime(2030, 1, 1)},
9377+
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": future_datetime},
94069378
)
94079379
snapshot.match("put-object-retention-no-key", e.value.response)
94089380

@@ -9431,26 +9403,38 @@ def test_s3_object_retention_exc(self, aws_client, s3_create_bucket, snapshot):
94319403
aws_client.s3.put_object_retention(
94329404
Bucket=s3_bucket_locked,
94339405
Key=object_key,
9434-
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": datetime.datetime(2030, 1, 1)},
9406+
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": future_datetime},
94359407
)
9436-
# update a retention without bypass
9408+
9409+
# update a retention to be lower than the existing one without bypass
9410+
earlier_datetime = future_datetime - datetime.timedelta(days=365)
94379411
with pytest.raises(ClientError) as e:
94389412
aws_client.s3.put_object_retention(
94399413
Bucket=s3_bucket_locked,
94409414
Key=object_key,
94419415
VersionId=version_id,
9442-
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": datetime.datetime(2025, 1, 1)},
9416+
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": earlier_datetime},
94439417
)
94449418
snapshot.match("update-retention-no-bypass", e.value.response)
94459419

9420+
# update a retention with date in the past
9421+
with pytest.raises(ClientError) as e:
9422+
aws_client.s3.put_object_retention(
9423+
Bucket=s3_bucket_locked,
9424+
Key=object_key,
9425+
VersionId=version_id,
9426+
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": datetime.datetime(2020, 1, 1)},
9427+
)
9428+
snapshot.match("update-retention-past-date", e.value.response)
9429+
94469430
s3_bucket_basic = s3_create_bucket(ObjectLockEnabledForBucket=False) # same as default
94479431
aws_client.s3.put_object(Bucket=s3_bucket_basic, Key=object_key, Body="test")
94489432
# put object retention in a object in bucket without lock configured
94499433
with pytest.raises(ClientError) as e:
94509434
aws_client.s3.put_object_retention(
94519435
Bucket=s3_bucket_basic,
94529436
Key=object_key,
9453-
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": datetime.datetime(2030, 1, 1)},
9437+
Retention={"Mode": "GOVERNANCE", "RetainUntilDate": future_datetime},
94549438
)
94559439
snapshot.match("put-object-retention-regular-bucket", e.value.response)
94569440

‎tests/aws/services/s3/test_s3.snapshot.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9608,7 +9608,7 @@
96089608
}
96099609
},
96109610
"tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_s3_object_retention_exc": {
9611-
"recorded-date": "21-01-2025, 18:17:43",
9611+
"recorded-date": "23-01-2025, 11:12:34",
96129612
"recorded-content": {
96139613
"put-object-retention-no-bucket": {
96149614
"Error": {
@@ -9653,9 +9653,19 @@
96539653
}
96549654
},
96559655
"update-retention-no-bypass": {
9656+
"Error": {
9657+
"Code": "AccessDenied",
9658+
"Message": "Access Denied because object protected by object lock."
9659+
},
9660+
"ResponseMetadata": {
9661+
"HTTPHeaders": {},
9662+
"HTTPStatusCode": 403
9663+
}
9664+
},
9665+
"update-retention-past-date": {
96569666
"Error": {
96579667
"ArgumentName": "RetainUntilDate",
9658-
"ArgumentValue": "Tue Dec 31 16:00:00 PST 2024",
9668+
"ArgumentValue": "Tue Dec 31 16:00:00 PST 2019",
96599669
"Code": "InvalidArgument",
96609670
"Message": "The retain until date must be in the future!"
96619671
},

‎tests/aws/services/s3/test_s3.validation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@
594594
"last_validated_date": "2025-01-21T18:17:58+00:00"
595595
},
596596
"tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_s3_object_retention_exc": {
597-
"last_validated_date": "2025-01-21T18:17:43+00:00"
597+
"last_validated_date": "2025-01-23T11:12:34+00:00"
598598
},
599599
"tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_eq": {
600600
"last_validated_date": "2024-09-23T11:02:16+00:00"

0 commit comments

Comments
 (0)