Enforce dynamodb key size limit [solves #3866] (#3888)

* add tests for dynamodb max key size

correct too-large error for ddb key

* remove unnecessary requires_boto_gte decorator from ddb tests

* remove literal emoji from ddb test

* implement dynamodb key limits, WIP

* correct direction of dynamodb range key length check

* fix tests for dynamodb max key size check

* catch ddb validation errors and rethrow properly

* finish ddb key size limit fixes

* fix linting

* handle unicode in v2.7 tests

* fix encoding issue in py2.7 for ddb

* linting

* Python2/3 compatability

Co-authored-by: Bert Blommers <info@bertblommers.nl>
This commit is contained in:
Matthew Davis 2021-04-30 22:47:47 +10:00 committed by GitHub
commit d6384fcb35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 359 additions and 28 deletions

View file

@ -16,6 +16,7 @@ from tests.helpers import requires_boto_gte
import moto.dynamodb2.comparisons
import moto.dynamodb2.models
from moto.dynamodb2.limits import HASH_KEY_MAX_LENGTH, RANGE_KEY_MAX_LENGTH
import pytest
@ -184,9 +185,8 @@ def test_list_not_found_table_tags():
assert exception.response["Error"]["Code"] == "ResourceNotFoundException"
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_item_add_empty_string_in_key_exception():
def test_item_add_empty_string_hash_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
@ -220,9 +220,49 @@ def test_item_add_empty_string_in_key_exception():
)
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_item_add_empty_string_no_exception():
def test_item_add_empty_string_range_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "ReceivedTime", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "ReceivedTime", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
with pytest.raises(ClientError) as ex:
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "someone@somewhere.edu"},
"ReceivedTime": {"S": ""},
},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: An AttributeValue may not contain an empty string"
)
@mock_dynamodb2
def test_item_add_empty_string_attr_no_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
@ -249,52 +289,8 @@ def test_item_add_empty_string_no_exception():
)
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_update_item_with_empty_string_in_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "test"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
with pytest.raises(ClientError) as ex:
conn.update_item(
TableName=name,
Key={"forum_name": {"S": "LOLCat Forum"}},
UpdateExpression="set forum_name=:NewName",
ExpressionAttributeValues={":NewName": {"S": ""}},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: An AttributeValue may not contain an empty string"
)
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_update_item_with_empty_string_no_exception():
def test_update_item_with_empty_string_attr_no_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
@ -328,6 +324,303 @@ def test_update_item_with_empty_string_no_exception():
)
@mock_dynamodb2
def test_item_add_long_string_hash_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "x" * HASH_KEY_MAX_LENGTH},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "test"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
with pytest.raises(ClientError) as ex:
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "x" * (HASH_KEY_MAX_LENGTH + 1)},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "test"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# deliberately no space between "of" and "2048"
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: Size of hashkey has exceeded the maximum size limit of2048 bytes"
)
@mock_dynamodb2
def test_item_add_long_string_nonascii_hash_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
emoji_b = b"\xf0\x9f\x98\x83" # smile emoji
emoji = emoji_b.decode("utf-8") # 1 character, but 4 bytes
short_enough = emoji * int(HASH_KEY_MAX_LENGTH / len(emoji.encode("utf-8")))
too_long = "x" + short_enough
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": short_enough},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "test"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
with pytest.raises(ClientError) as ex:
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": too_long},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "test"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# deliberately no space between "of" and "2048"
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: Size of hashkey has exceeded the maximum size limit of2048 bytes"
)
@mock_dynamodb2
def test_item_add_long_string_range_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "ReceivedTime", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "ReceivedTime", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "someone@somewhere.edu"},
"ReceivedTime": {"S": "x" * RANGE_KEY_MAX_LENGTH},
},
)
with pytest.raises(ClientError) as ex:
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "someone@somewhere.edu"},
"ReceivedTime": {"S": "x" * (RANGE_KEY_MAX_LENGTH + 1)},
},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: Aggregated size of all range keys has exceeded the size limit of 1024 bytes"
)
@mock_dynamodb2
def test_item_add_long_string_range_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "ReceivedTime", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "ReceivedTime", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "someone@somewhere.edu"},
"ReceivedTime": {"S": "x" * RANGE_KEY_MAX_LENGTH},
},
)
with pytest.raises(ClientError) as ex:
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "someone@somewhere.edu"},
"ReceivedTime": {"S": "x" * (RANGE_KEY_MAX_LENGTH + 1)},
},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: Aggregated size of all range keys has exceeded the size limit of 1024 bytes"
)
@mock_dynamodb2
def test_update_item_with_long_string_hash_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.update_item(
TableName=name,
Key={
"forum_name": {"S": "x" * HASH_KEY_MAX_LENGTH},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
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"},
},
UpdateExpression="set body=:New",
ExpressionAttributeValues={":New": {"S": "hello"}},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# deliberately no space between "of" and "2048"
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: Size of hashkey has exceeded the maximum size limit of2048 bytes"
)
@mock_dynamodb2
def test_update_item_with_long_string_range_key_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "ReceivedTime", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "ReceivedTime", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.update_item(
TableName=name,
Key={
"forum_name": {"S": "Lolcat Forum"},
"ReceivedTime": {"S": "x" * RANGE_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": "Lolcat Forum"},
"ReceivedTime": {"S": "x" * (RANGE_KEY_MAX_LENGTH + 1)},
},
UpdateExpression="set body=:New",
ExpressionAttributeValues={":New": {"S": "hello"}},
)
ex.value.response["Error"]["Code"].should.equal("ValidationException")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# deliberately no space between "of" and "2048"
ex.value.response["Error"]["Message"].should.equal(
"One or more parameter values were invalid: Aggregated size of all range keys has exceeded the size limit of 1024 bytes"
)
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_query_invalid_table():