From 23542b1b16f84e905a76eb631f8f9da0b2ad3c16 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Tue, 24 Jan 2023 14:33:34 -0100 Subject: [PATCH] DynamoDB: transact_write_items() errors on empty GSI keys (#5873) --- moto/dynamodb/models/table.py | 1 + moto/dynamodb/responses.py | 52 ++++++++++++++----- .../exceptions/test_dynamodb_exceptions.py | 39 ++++++++++++++ 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py index 483897246..6c95500ce 100644 --- a/moto/dynamodb/models/table.py +++ b/moto/dynamodb/models/table.py @@ -34,6 +34,7 @@ class SecondaryIndex(BaseModel): self.schema = schema self.table_key_attrs = table_key_attrs self.projection = projection + self.schema_key_attrs = [k["AttributeName"] for k in schema] def project(self, item: Item) -> Item: """ diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 54f3abe72..2108f4b4f 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -79,12 +79,17 @@ def include_consumed_capacity( return _inner -def get_empty_keys_on_put(field_updates: Dict[str, Any], table: Table) -> Optional[str]: +def validate_put_has_empty_keys( + field_updates: Dict[str, Any], table: Table, custom_error_msg: Optional[str] = None +) -> None: """ - Return the first key-name that has an empty value. None if all keys are filled + Error if any keys have an empty value. Checks Global index attributes as well """ if table: key_names = table.attribute_keys + gsi_key_names = list( + itertools.chain(*[gsi.schema_key_attrs for gsi in table.global_indexes]) + ) # string/binary fields with empty string as value empty_str_fields = [ @@ -92,10 +97,27 @@ def get_empty_keys_on_put(field_updates: Dict[str, Any], table: Table) -> Option for (key, val) in field_updates.items() if next(iter(val.keys())) in ["S", "B"] and next(iter(val.values())) == "" ] - return next( + + # First validate that all of the GSI-keys are set + empty_gsi_key = next( + (kn for kn in gsi_key_names if kn in empty_str_fields), None + ) + if empty_gsi_key: + gsi_name = table.global_indexes[0].name + raise MockValidationException( + f"One or more parameter values are not valid. A value specified for a secondary index key is not supported. The AttributeValue for a key attribute cannot contain an empty string value. IndexName: {gsi_name}, IndexKey: {empty_gsi_key}" + ) + + # Then validate that all of the regular keys are set + empty_key = next( (keyname for keyname in key_names if keyname in empty_str_fields), None ) - return None + if empty_key: + msg = ( + custom_error_msg + or "One or more parameter values were invalid: An AttributeValue may not contain an empty string. Key: {}" + ) + raise MockValidationException(msg.format(empty_key)) def put_has_empty_attrs(field_updates: Dict[str, Any], table: Table) -> bool: @@ -401,11 +423,7 @@ class DynamoHandler(BaseResponse): raise MockValidationException("Return values set to invalid value") table = self.dynamodb_backend.get_table(name) - empty_key = get_empty_keys_on_put(item, table) - if empty_key: - raise MockValidationException( - f"One or more parameter values were invalid: An AttributeValue may not contain an empty string. Key: {empty_key}" - ) + validate_put_has_empty_keys(item, table) if put_has_empty_attrs(item, table): raise MockValidationException( "One or more parameter values were invalid: An number set may not be empty" @@ -462,11 +480,11 @@ class DynamoHandler(BaseResponse): request = list(table_request.values())[0] if request_type == "PutRequest": item = request["Item"] - empty_key = get_empty_keys_on_put(item, table) - if empty_key: - raise MockValidationException( - f"One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: {empty_key}" - ) + validate_put_has_empty_keys( + item, + table, + custom_error_msg="One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: {}", + ) put_requests.append((table_name, item)) elif request_type == "DeleteRequest": keys = request["Key"] @@ -1004,6 +1022,12 @@ class DynamoHandler(BaseResponse): def transact_write_items(self) -> str: transact_items = self.body["TransactItems"] + # Validate first - we should error before we start the transaction + for item in transact_items: + if "Put" in item: + item_attrs = item["Put"]["Item"] + table = self.dynamodb_backend.get_table(item["Put"]["TableName"]) + validate_put_has_empty_keys(item_attrs, table) self.dynamodb_backend.transact_write_items(transact_items) response: Dict[str, Any] = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}} return dynamo_json_dump(response) diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py index ebb7c13cc..4f4cc7489 100644 --- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py +++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py @@ -803,6 +803,45 @@ def test_transact_write_items_multiple_operations_fail(): ) +@mock_dynamodb +def test_transact_write_items_with_empty_gsi_key(): + client = boto3.client("dynamodb", "us-east-2") + + client.create_table( + TableName="test_table", + KeySchema=[{"AttributeName": "unique_code", "KeyType": "HASH"}], + AttributeDefinitions=[ + {"AttributeName": "unique_code", "AttributeType": "S"}, + {"AttributeName": "unique_id", "AttributeType": "S"}, + ], + GlobalSecondaryIndexes=[ + { + "IndexName": "gsi_index", + "KeySchema": [{"AttributeName": "unique_id", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + } + ], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + + transact_items = [ + { + "Put": { + "Item": {"unique_code": {"S": "some code"}, "unique_id": {"S": ""}}, + "TableName": "test_table", + } + } + ] + + with pytest.raises(ClientError) as exc: + client.transact_write_items(TransactItems=transact_items) + err = exc.value.response["Error"] + err["Code"].should.equal("ValidationException") + err["Message"].should.equal( + "One or more parameter values are not valid. A value specified for a secondary index key is not supported. The AttributeValue for a key attribute cannot contain an empty string value. IndexName: gsi_index, IndexKey: unique_id" + ) + + @mock_dynamodb def test_update_primary_key_with_sortkey(): dynamodb = boto3.resource("dynamodb", region_name="us-east-1")