From 20abb764a23b80b516654b58e405e83991595563 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Fri, 24 Nov 2023 20:06:38 -0100 Subject: [PATCH] DynamoDB: Query with KeyConditionExpression now throws exception on empty keys (#7065) --- moto/dynamodb/exceptions.py | 7 ++++ .../parsing/key_condition_expression.py | 15 +++++--- moto/dynamodb/responses.py | 6 +-- .../exceptions/test_key_length_exceptions.py | 38 +++++++++++++++++++ 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/moto/dynamodb/exceptions.py b/moto/dynamodb/exceptions.py index 57823cfee293..029e8eb328a4 100644 --- a/moto/dynamodb/exceptions.py +++ b/moto/dynamodb/exceptions.py @@ -19,6 +19,13 @@ def __init__(self, message: str): self.exception_msg = message +class KeyIsEmptyStringException(MockValidationException): + def __init__(self, empty_key: str): + super().__init__( + message=f"One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: {empty_key}" + ) + + class InvalidIndexNameError(MockValidationException): pass diff --git a/moto/dynamodb/parsing/key_condition_expression.py b/moto/dynamodb/parsing/key_condition_expression.py index 7638f0e24c8c..2885f7652602 100644 --- a/moto/dynamodb/parsing/key_condition_expression.py +++ b/moto/dynamodb/parsing/key_condition_expression.py @@ -1,6 +1,6 @@ from enum import Enum from typing import Any, List, Dict, Tuple, Optional, Union -from moto.dynamodb.exceptions import MockValidationException +from moto.dynamodb.exceptions import MockValidationException, KeyIsEmptyStringException from moto.utilities.tokenizer import GenericTokenizer @@ -209,6 +209,8 @@ def validate_schema( ) if comparison != "=": raise MockValidationException("Query key condition not supported") + if "S" in hash_value and hash_value["S"] == "": + raise KeyIsEmptyStringException(index_hash_key) # type: ignore[arg-type] index_range_key = get_key(schema, "RANGE") range_key, range_comparison, range_values = next( @@ -219,9 +221,12 @@ def validate_schema( ), (None, None, []), ) - if index_range_key and len(results) > 1 and range_key != index_range_key: - raise MockValidationException( - f"Query condition missed key schema element: {index_range_key}" - ) + if index_range_key: + if len(results) > 1 and range_key != index_range_key: + raise MockValidationException( + f"Query condition missed key schema element: {index_range_key}" + ) + if {"S": ""} in range_values: + raise KeyIsEmptyStringException(index_range_key) return hash_value, range_comparison, range_values # type: ignore[return-value] diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index a751aad8cd2b..1cf70b3509f7 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -14,6 +14,7 @@ MockValidationException, ResourceNotFoundException, UnknownKeyType, + KeyIsEmptyStringException, ) from moto.dynamodb.models import dynamodb_backends, Table, DynamoDBBackend from moto.dynamodb.models.utilities import dynamo_json_dump @@ -554,10 +555,7 @@ def get_item(self) -> str: key = self.body["Key"] empty_keys = [k for k, v in key.items() if not next(iter(v.values()))] if empty_keys: - raise MockValidationException( - "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an " - f"empty string value. Key: {empty_keys[0]}" - ) + raise KeyIsEmptyStringException(empty_keys[0]) projection_expression = self._get_projection_expression() attributes_to_get = self.body.get("AttributesToGet") diff --git a/tests/test_dynamodb/exceptions/test_key_length_exceptions.py b/tests/test_dynamodb/exceptions/test_key_length_exceptions.py index f3749f846e2a..90c200855d18 100644 --- a/tests/test_dynamodb/exceptions/test_key_length_exceptions.py +++ b/tests/test_dynamodb/exceptions/test_key_length_exceptions.py @@ -3,6 +3,7 @@ from moto import mock_dynamodb from botocore.exceptions import ClientError +from boto3.dynamodb.conditions import Key from moto.dynamodb.limits import HASH_KEY_MAX_LENGTH, RANGE_KEY_MAX_LENGTH @@ -323,3 +324,40 @@ def test_item_add_empty_key_exception(): ex.value.response["Error"]["Message"] == "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: forum_name" ) + + +@mock_dynamodb +def test_query_empty_key_exception(): + name = "TestTable" + conn = boto3.client("dynamodb", region_name="us-west-2") + conn.create_table( + TableName=name, + KeySchema=[ + {"AttributeName": "hk", "KeyType": "HASH"}, + {"AttributeName": "rk", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "hk", "AttributeType": "S"}, + {"AttributeName": "rk", "AttributeType": "S"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = boto3.resource("dynamodb", "us-west-2").Table(name) + + with pytest.raises(ClientError) as ex: + table.query(KeyConditionExpression=Key("hk").eq("")) + assert ex.value.response["Error"]["Code"] == "ValidationException" + assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400 + assert ( + ex.value.response["Error"]["Message"] + == "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: hk" + ) + + with pytest.raises(ClientError) as ex: + table.query(KeyConditionExpression=Key("hk").eq("sth") & Key("rk").eq("")) + assert ex.value.response["Error"]["Code"] == "ValidationException" + assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400 + assert ( + ex.value.response["Error"]["Message"] + == "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: rk" + )