From 6e7d3ec938a6ca5e8b2c9ec25d9a0f066ac6e06d Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sat, 14 Jan 2023 14:51:47 -0100 Subject: [PATCH] DynamoDB: Add validation for GSI attrs set to None (#5843) --- moto/dynamodb/models/__init__.py | 2 +- moto/dynamodb/responses.py | 20 ++++++++-- .../exceptions/test_dynamodb_exceptions.py | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py index 854727ee7..9360a4155 100644 --- a/moto/dynamodb/models/__init__.py +++ b/moto/dynamodb/models/__init__.py @@ -1422,7 +1422,7 @@ class DynamoDBBackend(BaseBackend): else: return table.schema - def get_table(self, table_name): + def get_table(self, table_name) -> Table: if table_name not in self.tables: raise ResourceNotFoundException() return self.tables.get(table_name) diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 1b4fb83f8..0144c835b 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -13,7 +13,7 @@ from .exceptions import ( ResourceNotFoundException, ConditionalCheckFailed, ) -from moto.dynamodb.models import dynamodb_backends, dynamo_json_dump +from moto.dynamodb.models import dynamodb_backends, dynamo_json_dump, Table from moto.utilities.aws_headers import amz_crc32, amzn_request_id @@ -67,7 +67,7 @@ def include_consumed_capacity(val=1.0): return _inner -def get_empty_keys_on_put(field_updates, table): +def get_empty_keys_on_put(field_updates, table: Table): """ Return the first key-name that has an empty value. None if all keys are filled """ @@ -105,6 +105,16 @@ def put_has_empty_attrs(field_updates, table): return False +def validate_put_has_gsi_keys_set_to_none(item, table: Table) -> None: + for gsi in table.global_indexes: + for attr in gsi.schema: + attr_name = attr["AttributeName"] + if attr_name in item and item[attr_name] == {"NULL": True}: + raise MockValidationException( + f"One or more parameter values were invalid: Type mismatch for Index Key {attr_name} Expected: S Actual: NULL IndexName: {gsi.name}" + ) + + def check_projection_expression(expression): if expression.upper() in ReservedKeywords.get_reserved_keywords(): raise MockValidationException( @@ -375,15 +385,17 @@ class DynamoHandler(BaseResponse): if return_values not in ("ALL_OLD", "NONE"): raise MockValidationException("Return values set to invalid value") - empty_key = get_empty_keys_on_put(item, self.dynamodb_backend.get_table(name)) + 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}" ) - if put_has_empty_attrs(item, self.dynamodb_backend.get_table(name)): + if put_has_empty_attrs(item, table): raise MockValidationException( "One or more parameter values were invalid: An number set may not be empty" ) + validate_put_has_gsi_keys_set_to_none(item, table) overwrite = "Expected" not in self.body if not overwrite: diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py index 6271f7678..ebb7c13cc 100644 --- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py +++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py @@ -899,3 +899,43 @@ def test_put_item__string_as_integer_value(): err = exc.value.response["Error"] err["Code"].should.equal("SerializationException") err["Message"].should.equal("Start of structure or map found where not expected") + + +@mock_dynamodb +def test_gsi_key_cannot_be_empty(): + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") + hello_index = { + "IndexName": "hello-index", + "KeySchema": [{"AttributeName": "hello", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + } + table_name = "lilja-test" + + # Let's create a table with [id: str, hello: str], with an index to hello + dynamodb.create_table( + TableName=table_name, + KeySchema=[ + {"AttributeName": "id", "KeyType": "HASH"}, + ], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "hello", "AttributeType": "S"}, + ], + GlobalSecondaryIndexes=[hello_index], + BillingMode="PAY_PER_REQUEST", + ) + + table = dynamodb.Table(table_name) + with pytest.raises(ClientError) as exc: + table.put_item( + TableName=table_name, + Item={ + "id": "woop", + "hello": None, + }, + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ValidationException") + err["Message"].should.equal( + "One or more parameter values were invalid: Type mismatch for Index Key hello Expected: S Actual: NULL IndexName: hello-index" + )