Skip to content

Commit

Permalink
DynamoDB: get/delete/update_item() now validates all provided keys ex…
Browse files Browse the repository at this point in the history
…ist (#8139)
  • Loading branch information
bblommers authored Sep 22, 2024
1 parent 78d139c commit 25da940
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 34 deletions.
14 changes: 12 additions & 2 deletions moto/dynamodb/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .exceptions import (
KeyIsEmptyStringException,
MockValidationException,
ProvidedKeyDoesNotExist,
ResourceNotFoundException,
UnknownKeyType,
)
Expand Down Expand Up @@ -565,7 +566,7 @@ def batch_write_item(self) -> str:
@include_consumed_capacity(0.5)
def get_item(self) -> str:
name = self.body["TableName"]
self.dynamodb_backend.get_table(name)
table = self.dynamodb_backend.get_table(name)
key = self.body["Key"]
empty_keys = [k for k, v in key.items() if not next(iter(v.values()))]
if empty_keys:
Expand All @@ -589,6 +590,9 @@ def get_item(self) -> str:
"ExpressionAttributeNames must not be empty"
)

if not all([k in table.attribute_keys for k in key]):
raise ProvidedKeyDoesNotExist

expression_attribute_names = expression_attribute_names or {}
projection_expressions = self._adjust_projection_expression(
projection_expression, expression_attribute_names
Expand Down Expand Up @@ -857,7 +861,9 @@ def delete_item(self) -> str:
if return_values not in ("ALL_OLD", "NONE"):
raise MockValidationException("Return values set to invalid value")

self.dynamodb_backend.get_table(name)
table = self.dynamodb_backend.get_table(name)
if not all([k in table.attribute_keys for k in key]):
raise ProvidedKeyDoesNotExist

# Attempt to parse simple ConditionExpressions into an Expected
# expression
Expand Down Expand Up @@ -891,6 +897,10 @@ def update_item(self) -> str:
"Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}"
)

table = self.dynamodb_backend.get_table(name)
if not all([k in table.attribute_keys for k in key]):
raise ProvidedKeyDoesNotExist

if update_expression is not None:
update_expression = update_expression.strip()
if update_expression == "":
Expand Down
49 changes: 42 additions & 7 deletions tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,14 @@ def test_query_table_with_wrong_key_attribute_names_throws_exception():
assert err["Message"] == "Query condition missed key schema element: partitionKey"


@mock_aws
def test_empty_expressionattributenames():
@pytest.mark.aws_verified
@dynamodb_aws_verified()
def test_empty_expressionattributenames(table_name=None):
ddb = boto3.resource("dynamodb", region_name="us-east-1")
ddb.create_table(
TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
)
table = ddb.Table("test-table")
table = ddb.Table(table_name)
with pytest.raises(ClientError) as exc:
# Note: provided key is wrong
# Empty ExpressionAttributeName is verified earlier, so that's the error we get
table.get_item(Key={"id": "my_id"}, ExpressionAttributeNames={})
err = exc.value.response["Error"]
assert err["Code"] == "ValidationException"
Expand Down Expand Up @@ -777,7 +777,7 @@ def test_update_expression_with_trailing_comma():

with pytest.raises(ClientError) as exc:
table.update_item(
Key={"pk": "key", "sk": "sk"},
Key={"pk": "key"},
# Trailing comma should be invalid
UpdateExpression="SET #attr1 = :val1, #attr2 = :val2,",
ExpressionAttributeNames={"#attr1": "attr1", "#attr2": "attr2"},
Expand Down Expand Up @@ -1462,3 +1462,38 @@ def test_delete_table():
err.value.response["Error"]["Message"]
== "1 validation error detected: Table 'test1' can't be deleted while DeletionProtectionEnabled is set to True"
)


@pytest.mark.aws_verified
@dynamodb_aws_verified(add_range=False)
def test_provide_range_key_against_table_without_range_key(table_name=None):
ddb_client = boto3.client("dynamodb", "us-east-1")
with pytest.raises(ClientError) as exc:
ddb_client.get_item(
TableName=table_name, Key={"pk": {"S": "pk"}, "sk": {"S": "sk"}}
)
err = exc.value.response["Error"]
assert err["Code"] == "ValidationException"
assert err["Message"] == "The provided key element does not match the schema"

with pytest.raises(ClientError) as exc:
ddb_client.delete_item(
TableName=table_name, Key={"pk": {"S": "pk"}, "sk": {"S": "sk"}}
)
err = exc.value.response["Error"]
assert err["Code"] == "ValidationException"
assert err["Message"] == "The provided key element does not match the schema"

with pytest.raises(ClientError) as exc:
ddb_client.update_item(
TableName=table_name,
Key={
"pk": {"S": "x"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
UpdateExpression="set body=:New",
ExpressionAttributeValues={":New": {"S": "hello"}},
)
err = exc.value.response["Error"]
assert err["Code"] == "ValidationException"
assert err["Message"] == "The provided key element does not match the schema"
29 changes: 9 additions & 20 deletions tests/test_dynamodb/exceptions/test_key_length_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from moto import mock_aws
from moto.dynamodb.limits import HASH_KEY_MAX_LENGTH, RANGE_KEY_MAX_LENGTH
from tests.test_dynamodb import dynamodb_aws_verified


@mock_aws
Expand Down Expand Up @@ -215,34 +216,22 @@ def test_put_long_string_gsi_range_key_exception():
)


@mock_aws
def test_update_item_with_long_string_hash_key_exception():
name = "TestTable"
conn = boto3.client("dynamodb", region_name="us-west-2")
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
@pytest.mark.aws_verified
@dynamodb_aws_verified()
def test_update_item_with_long_string_hash_key_exception(table_name=None):
conn = boto3.client("dynamodb", region_name="us-east-1")

conn.update_item(
TableName=name,
Key={
"forum_name": {"S": "x" * HASH_KEY_MAX_LENGTH},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
TableName=table_name,
Key={"pk": {"S": "x" * HASH_KEY_MAX_LENGTH}},
UpdateExpression="set body=:New",
ExpressionAttributeValues={":New": {"S": "hello"}},
)

with pytest.raises(ClientError) as ex:
conn.update_item(
TableName=name,
Key={
"forum_name": {"S": "x" * (HASH_KEY_MAX_LENGTH + 1)},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
TableName=table_name,
Key={"pk": {"S": "x" * (HASH_KEY_MAX_LENGTH + 1)}},
UpdateExpression="set body=:New",
ExpressionAttributeValues={":New": {"S": "hello"}},
)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dynamodb/test_dynamodb_condition_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def update_if_attr_doesnt_exist():
# Test nonexistent top-level attribute.
client.update_item(
TableName="test",
Key={"forum_name": {"S": "the-key"}, "subject": {"S": "the-subject"}},
Key={"forum_name": {"S": "the-key"}},
UpdateExpression="set #new_state=:new_state, #ttl=:ttl",
ConditionExpression="attribute_not_exists(#new_state)",
ExpressionAttributeNames={"#new_state": "foobar", "#ttl": "ttl"},
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dynamodb/test_dynamodb_consumedcapacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_only_return_consumed_capacity_when_required(
validate_response(response, should_have_capacity, should_have_table)

# GET_ITEM
args = {"Key": item}
args = {"Key": {"job_id": item["job_id"]}}
if capacity:
args["ReturnConsumedCapacity"] = capacity
response = table.get_item(**args)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_dynamodb/test_dynamodb_update_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,9 @@ def test_update_expression_remove_list_and_attribute(table_name=None):
UpdateExpression="REMOVE #ulist[0], some_param",
ExpressionAttributeNames={"#ulist": "user_list"},
)
item = ddb_client.get_item(
TableName=table_name, Key={"pk": {"S": "primary_key"}, "sk": {"S": "sort_key"}}
)["Item"]
item = ddb_client.get_item(TableName=table_name, Key={"pk": {"S": "primary_key"}})[
"Item"
]
assert item == {
"user_list": {"L": [{"M": {"name": {"S": "Jane"}, "surname": {"S": "Smith"}}}]},
"pk": {"S": "primary_key"},
Expand Down

0 comments on commit 25da940

Please sign in to comment.