Skip to content

Commit

Permalink
DynamoDB: add checks for projection expression (#5380)
Browse files Browse the repository at this point in the history
  • Loading branch information
hajali-amine committed Aug 12, 2022
1 parent f91ffcf commit 7386163
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 14 deletions.
7 changes: 0 additions & 7 deletions moto/dynamodb/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1485,13 +1485,6 @@ def scan(
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,
Expand Down
37 changes: 30 additions & 7 deletions moto/dynamodb/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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,
Expand Down Expand Up @@ -100,6 +101,21 @@ def _validate_attr(attr: dict):
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
Expand Down Expand Up @@ -728,14 +744,17 @@ def _adjust(expression):
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

Expand All @@ -760,6 +779,10 @@ def scan(self):
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,
Expand Down
60 changes: 60 additions & 0 deletions tests/test_dynamodb/test_dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

0 comments on commit 7386163

Please sign in to comment.