diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py index de9c51cea..a5f49b66e 100644 --- a/moto/dynamodb/models/__init__.py +++ b/moto/dynamodb/models/__init__.py @@ -1485,13 +1485,6 @@ class DynamoDBBackend(BaseBackend): filter_expression, expr_names, expr_values ) - projection_expression = ",".join( - [ - expr_names.get(attr, attr) - for attr in projection_expression.replace(" ", "").split(",") - ] - ) - return table.scan( scan_filters, limit, diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 38435a99b..93fa9b51a 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -7,6 +7,7 @@ from functools import wraps from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores, amz_crc32, amzn_request_id +from moto.dynamodb.parsing.reserved_keywords import ReservedKeywords from .exceptions import ( MockValidationException, ResourceNotFoundException, @@ -100,6 +101,21 @@ def put_has_empty_attrs(field_updates, table): return False +def check_projection_expression(expression): + if expression.upper() in ReservedKeywords.get_reserved_keywords(): + raise MockValidationException( + f"ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: {expression}" + ) + if expression[0].isnumeric(): + raise MockValidationException( + "ProjectionExpression: Attribute name starts with a number" + ) + if " " in expression: + raise MockValidationException( + "ProjectionExpression: Attribute name contains white space" + ) + + class DynamoHandler(BaseResponse): def get_endpoint_name(self, headers): """Parses request headers and extracts part od the X-Amz-Target @@ -728,14 +744,17 @@ class DynamoHandler(BaseResponse): else expression ) - if projection_expression and expr_attr_names: + if projection_expression: expressions = [x.strip() for x in projection_expression.split(",")] - return ",".join( - [ - ".".join([_adjust(expr) for expr in nested_expr.split(".")]) - for nested_expr in expressions - ] - ) + for expression in expressions: + check_projection_expression(expression) + if expr_attr_names: + return ",".join( + [ + ".".join([_adjust(expr) for expr in nested_expr.split(".")]) + for nested_expr in expressions + ] + ) return projection_expression @@ -760,6 +779,10 @@ class DynamoHandler(BaseResponse): limit = self.body.get("Limit") index_name = self.body.get("IndexName") + projection_expression = self._adjust_projection_expression( + projection_expression, expression_attribute_names + ) + try: items, scanned_count, last_evaluated_key = self.dynamodb_backend.scan( name, diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 75e5515cc..d359e49f6 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -5780,3 +5780,63 @@ def test_projection_expression_execution_order(): ProjectionExpression="#a", ExpressionAttributeNames={"#a": "hashKey"}, ) + + +@mock_dynamodb +def test_invalid_projection_expressions(): + table_name = "test-projection-expressions-table" + client = boto3.client("dynamodb", region_name="us-east-1") + client.create_table( + TableName=table_name, + KeySchema=[{"AttributeName": "customer", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "customer", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + + with pytest.raises( + ClientError, + match="ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: name", + ): + client.scan(TableName=table_name, ProjectionExpression="name") + with pytest.raises( + ClientError, match="ProjectionExpression: Attribute name starts with a number" + ): + client.scan(TableName=table_name, ProjectionExpression="3ame") + with pytest.raises( + ClientError, match="ProjectionExpression: Attribute name contains white space" + ): + client.scan(TableName=table_name, ProjectionExpression="na me") + + with pytest.raises( + ClientError, + match="ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: name", + ): + client.get_item( + TableName=table_name, + Key={"customer": {"S": "a"}}, + ProjectionExpression="name", + ) + + with pytest.raises( + ClientError, + match="ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: name", + ): + client.query( + TableName=table_name, + KeyConditionExpression="a", + ProjectionExpression="name", + ) + + with pytest.raises( + ClientError, + match="ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: name", + ): + client.scan(TableName=table_name, ProjectionExpression="not_a_keyword, name") + with pytest.raises( + ClientError, match="ProjectionExpression: Attribute name starts with a number" + ): + client.scan(TableName=table_name, ProjectionExpression="not_a_keyword, 3ame") + with pytest.raises( + ClientError, match="ProjectionExpression: Attribute name contains white space" + ): + client.scan(TableName=table_name, ProjectionExpression="not_a_keyword, na me")