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

Skip to content

implement S3 native BucketLogging and BucketReplication #8901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
implement BucketLogging mocking
  • Loading branch information
bentsku committed Aug 16, 2023
commit 10b51d77ec3b71832e8d38a97101a8e24151f95c
3 changes: 2 additions & 1 deletion localstack/services/s3/v3/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class S3Bucket:
payer: Payer
encryption_rule: Optional[ServerSideEncryptionRule]
public_access_block: Optional[PublicAccessBlockConfiguration]
accelerate_status: BucketAccelerateStatus
accelerate_status: Optional[BucketAccelerateStatus]
object_lock_enabled: bool
object_ownership: ObjectOwnership
intelligent_tiering_configurations: dict[IntelligentTieringId, IntelligentTieringConfiguration]
Expand Down Expand Up @@ -142,6 +142,7 @@ def __init__(
self.public_access_block = DEFAULT_PUBLIC_BLOCK_ACCESS
self.multiparts = {}
self.notification_configuration = {}
self.logging = {}
self.cors_rules = None
self.lifecycle_rules = None
self.website_configuration = None
Expand Down
56 changes: 56 additions & 0 deletions localstack/services/s3/v3/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
BucketAlreadyOwnedByYou,
BucketCannedACL,
BucketLifecycleConfiguration,
BucketLoggingStatus,
BucketName,
BucketNotEmpty,
BypassGovernanceRetention,
Expand All @@ -45,6 +46,7 @@
CreateBucketRequest,
CreateMultipartUploadOutput,
CreateMultipartUploadRequest,
CrossLocationLoggingProhibitted,
Delete,
DeletedObject,
DeleteMarkerEntry,
Expand All @@ -64,6 +66,7 @@
GetBucketInventoryConfigurationOutput,
GetBucketLifecycleConfigurationOutput,
GetBucketLocationOutput,
GetBucketLoggingOutput,
GetBucketOwnershipControlsOutput,
GetBucketPolicyOutput,
GetBucketRequestPaymentOutput,
Expand Down Expand Up @@ -96,6 +99,7 @@
InvalidPartNumber,
InvalidPartOrder,
InvalidStorageClass,
InvalidTargetBucketForLogging,
InventoryConfiguration,
InventoryId,
KeyMarker,
Expand Down Expand Up @@ -3135,6 +3139,58 @@ def put_bucket_accelerate_configuration(

s3_bucket.accelerate_status = status

def put_bucket_logging(
self,
context: RequestContext,
bucket: BucketName,
bucket_logging_status: BucketLoggingStatus,
content_md5: ContentMD5 = None,
checksum_algorithm: ChecksumAlgorithm = None,
expected_bucket_owner: AccountId = None,
) -> None:
store = self.get_store(context.account_id, context.region)
if not (s3_bucket := store.buckets.get(bucket)):
raise NoSuchBucket("The specified bucket does not exist", BucketName=bucket)

if not (logging_config := bucket_logging_status.get("LoggingEnabled")):
s3_bucket.logging = {}
return

# the target bucket must be in the same account
if not (target_bucket_name := logging_config.get("TargetBucket")):
raise MalformedXML()

if not logging_config.get("TargetPrefix"):
logging_config["TargetPrefix"] = ""

# TODO: validate Grants

if not (target_s3_bucket := store.buckets.get(bucket)):
raise InvalidTargetBucketForLogging(
"The target bucket for logging does not exist",
TargetBucket=target_bucket_name,
)

if target_s3_bucket.bucket_region != s3_bucket.bucket_region:
raise CrossLocationLoggingProhibitted(
"Cross S3 location logging not allowed. ",
TargetBucketLocation=target_s3_bucket.bucket_region,
)

s3_bucket.logging = logging_config

def get_bucket_logging(
self, context: RequestContext, bucket: BucketName, expected_bucket_owner: AccountId = None
) -> GetBucketLoggingOutput:
store = self.get_store(context.account_id, context.region)
if not (s3_bucket := store.buckets.get(bucket)):
raise NoSuchBucket("The specified bucket does not exist", BucketName=bucket)

if not s3_bucket.logging:
return GetBucketLoggingOutput()

return GetBucketLoggingOutput(LoggingEnabled=s3_bucket.logging)

# ###### THIS ARE UNIMPLEMENTED METHODS TO ALLOW TESTING, DO NOT COUNT THEM AS DONE ###### #

def put_bucket_acl(
Expand Down
234 changes: 120 additions & 114 deletions tests/aws/s3/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -5199,120 +5199,6 @@ def test_s3_get_object_headers(self, aws_client, s3_create_bucket, snapshot):
aws_client.s3.get_object(Bucket=bucket, Key=key, IfMatch="etag")
snapshot.match("if_match_err_1", e.value.response["Error"])

@markers.aws.validated
def test_put_bucket_logging(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(
[
snapshot.transform.key_value("TargetBucket"),
snapshot.transform.key_value("DisplayName", reference_replacement=False),
snapshot.transform.key_value(
"ID", value_replacement="owner-id", reference_replacement=False
),
]
)

bucket_name = s3_create_bucket()
target_bucket = s3_create_bucket()
bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": target_bucket,
"TargetPrefix": "log",
},
}
resp = aws_client.s3.get_bucket_acl(Bucket=target_bucket)
snapshot.match("get-bucket-default-acl", resp)

# this might have been failing in the past, as the target bucket does not give access to LogDelivery to
# write/read_acp. however, AWS accepts it, because you can also set it with Permissions
resp = aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging", resp)

resp = aws_client.s3.get_bucket_logging(Bucket=bucket_name)
snapshot.match("get-bucket-logging", resp)

# delete BucketLogging
resp = aws_client.s3.put_bucket_logging(Bucket=bucket_name, BucketLoggingStatus={})
snapshot.match("put-bucket-logging-delete", resp)

@markers.aws.validated
def test_put_bucket_logging_accept_wrong_grants(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(snapshot.transform.key_value("TargetBucket"))

bucket_name = s3_create_bucket()

target_bucket = s3_create_bucket()
# We need to delete the ObjectOwnership from the bucket, because you otherwise can't set TargetGrants on it
# TODO: have the same default as AWS and have ObjectOwnership set
aws_client.s3.delete_bucket_ownership_controls(Bucket=target_bucket)

bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": target_bucket,
"TargetPrefix": "log",
"TargetGrants": [
{
"Grantee": {
"URI": "http://acs.amazonaws.com/groups/s3/LogDelivery",
"Type": "Group",
},
"Permission": "WRITE",
},
{
"Grantee": {
"URI": "http://acs.amazonaws.com/groups/s3/LogDelivery",
"Type": "Group",
},
"Permission": "READ_ACP",
},
],
},
}

# from the documentation, only WRITE | READ | FULL_CONTROL are allowed, but AWS let READ_ACP pass
resp = aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging", resp)

resp = aws_client.s3.get_bucket_logging(Bucket=bucket_name)
snapshot.match("get-bucket-logging", resp)

@markers.aws.validated
def test_put_bucket_logging_wrong_target(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(snapshot.transform.key_value("TargetBucket"))
bucket_name = s3_create_bucket()
target_bucket = s3_create_bucket(
CreateBucketConfiguration={"LocationConstraint": "us-west-2"}
)

with pytest.raises(ClientError) as e:
bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": target_bucket,
"TargetPrefix": "log",
},
}
aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging-different-regions", e.value.response)

nonexistent_target_bucket = f"target-bucket-{long_uid()}"
with pytest.raises(ClientError) as e:
bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": nonexistent_target_bucket,
"TargetPrefix": "log",
},
}
aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging-non-existent-bucket", e.value.response)
assert e.value.response["Error"]["TargetBucket"] == nonexistent_target_bucket

@markers.aws.validated
def test_s3_inventory_report_crud(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(snapshot.transform.resource_name())
Expand Down Expand Up @@ -9421,6 +9307,126 @@ def test_s3_copy_object_legal_hold(self, s3_create_bucket, snapshot, aws_client)
)


class TestS3BucketLogging:
@markers.aws.validated
def test_put_bucket_logging(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(
[
snapshot.transform.key_value("TargetBucket"),
snapshot.transform.key_value("DisplayName", reference_replacement=False),
snapshot.transform.key_value(
"ID", value_replacement="owner-id", reference_replacement=False
),
]
)

bucket_name = s3_create_bucket()
target_bucket = s3_create_bucket()

resp = aws_client.s3.get_bucket_logging(Bucket=bucket_name)
snapshot.match("get-bucket-logging-default", resp)

bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": target_bucket,
"TargetPrefix": "log",
},
}
resp = aws_client.s3.get_bucket_acl(Bucket=target_bucket)
snapshot.match("get-bucket-default-acl", resp)

# this might have been failing in the past, as the target bucket does not give access to LogDelivery to
# write/read_acp. however, AWS accepts it, because you can also set it with Permissions
resp = aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging", resp)

resp = aws_client.s3.get_bucket_logging(Bucket=bucket_name)
snapshot.match("get-bucket-logging", resp)

# delete BucketLogging
resp = aws_client.s3.put_bucket_logging(Bucket=bucket_name, BucketLoggingStatus={})
snapshot.match("put-bucket-logging-delete", resp)

@markers.aws.validated
def test_put_bucket_logging_accept_wrong_grants(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(snapshot.transform.key_value("TargetBucket"))

bucket_name = s3_create_bucket()

target_bucket = s3_create_bucket()
# We need to delete the ObjectOwnership from the bucket, because you otherwise can't set TargetGrants on it
# TODO: have the same default as AWS and have ObjectOwnership set
aws_client.s3.delete_bucket_ownership_controls(Bucket=target_bucket)

bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": target_bucket,
"TargetPrefix": "log",
"TargetGrants": [
{
"Grantee": {
"URI": "http://acs.amazonaws.com/groups/s3/LogDelivery",
"Type": "Group",
},
"Permission": "WRITE",
},
{
"Grantee": {
"URI": "http://acs.amazonaws.com/groups/s3/LogDelivery",
"Type": "Group",
},
"Permission": "READ_ACP",
},
],
},
}

# from the documentation, only WRITE | READ | FULL_CONTROL are allowed, but AWS let READ_ACP pass
resp = aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging", resp)

resp = aws_client.s3.get_bucket_logging(Bucket=bucket_name)
snapshot.match("get-bucket-logging", resp)

@markers.aws.validated
def test_put_bucket_logging_wrong_target(self, aws_client, s3_create_bucket, snapshot):
snapshot.add_transformer(snapshot.transform.key_value("TargetBucket"))
bucket_name = s3_create_bucket()
target_bucket = s3_create_bucket(
CreateBucketConfiguration={"LocationConstraint": "us-west-2"}
)

with pytest.raises(ClientError) as e:
bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": target_bucket,
"TargetPrefix": "log",
},
}
aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging-different-regions", e.value.response)

nonexistent_target_bucket = f"target-bucket-{long_uid()}"
with pytest.raises(ClientError) as e:
bucket_logging_status = {
"LoggingEnabled": {
"TargetBucket": nonexistent_target_bucket,
"TargetPrefix": "log",
},
}
aws_client.s3.put_bucket_logging(
Bucket=bucket_name, BucketLoggingStatus=bucket_logging_status
)
snapshot.match("put-bucket-logging-non-existent-bucket", e.value.response)
assert e.value.response["Error"]["TargetBucket"] == nonexistent_target_bucket


def _s3_client_custom_config(conf: Config, endpoint_url: str = None):
if os.environ.get("TEST_TARGET") == "AWS_CLOUD":
return boto3.client("s3", config=conf, endpoint_url=endpoint_url)
Expand Down
14 changes: 10 additions & 4 deletions tests/aws/s3/test_s3.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -7573,9 +7573,15 @@
}
}
},
"tests/aws/s3/test_s3.py::TestS3::test_put_bucket_logging": {
"recorded-date": "03-08-2023, 04:26:09",
"tests/aws/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging": {
"recorded-date": "12-08-2023, 19:54:07",
"recorded-content": {
"get-bucket-logging-default": {
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"get-bucket-default-acl": {
"Grants": [
{
Expand Down Expand Up @@ -7620,7 +7626,7 @@
}
}
},
"tests/aws/s3/test_s3.py::TestS3::test_put_bucket_logging_accept_wrong_grants": {
"tests/aws/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging_accept_wrong_grants": {
"recorded-date": "03-08-2023, 04:26:11",
"recorded-content": {
"put-bucket-logging": {
Expand Down Expand Up @@ -7657,7 +7663,7 @@
}
}
},
"tests/aws/s3/test_s3.py::TestS3::test_put_bucket_logging_wrong_target": {
"tests/aws/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging_wrong_target": {
"recorded-date": "03-08-2023, 04:26:14",
"recorded-content": {
"put-bucket-logging-different-regions": {
Expand Down