DynamoDB: transact_write_items() errors on empty GSI keys (#5873)
This commit is contained in:
		
							parent
							
								
									7786bf3c23
								
							
						
					
					
						commit
						23542b1b16
					
				| @ -34,6 +34,7 @@ class SecondaryIndex(BaseModel): | |||||||
|         self.schema = schema |         self.schema = schema | ||||||
|         self.table_key_attrs = table_key_attrs |         self.table_key_attrs = table_key_attrs | ||||||
|         self.projection = projection |         self.projection = projection | ||||||
|  |         self.schema_key_attrs = [k["AttributeName"] for k in schema] | ||||||
| 
 | 
 | ||||||
|     def project(self, item: Item) -> Item: |     def project(self, item: Item) -> Item: | ||||||
|         """ |         """ | ||||||
|  | |||||||
| @ -79,12 +79,17 @@ def include_consumed_capacity( | |||||||
|     return _inner |     return _inner | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_empty_keys_on_put(field_updates: Dict[str, Any], table: Table) -> Optional[str]: | def validate_put_has_empty_keys( | ||||||
|  |     field_updates: Dict[str, Any], table: Table, custom_error_msg: Optional[str] = None | ||||||
|  | ) -> None: | ||||||
|     """ |     """ | ||||||
|     Return the first key-name that has an empty value. None if all keys are filled |     Error if any keys have an empty value. Checks Global index attributes as well | ||||||
|     """ |     """ | ||||||
|     if table: |     if table: | ||||||
|         key_names = table.attribute_keys |         key_names = table.attribute_keys | ||||||
|  |         gsi_key_names = list( | ||||||
|  |             itertools.chain(*[gsi.schema_key_attrs for gsi in table.global_indexes]) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         # string/binary fields with empty string as value |         # string/binary fields with empty string as value | ||||||
|         empty_str_fields = [ |         empty_str_fields = [ | ||||||
| @ -92,10 +97,27 @@ def get_empty_keys_on_put(field_updates: Dict[str, Any], table: Table) -> Option | |||||||
|             for (key, val) in field_updates.items() |             for (key, val) in field_updates.items() | ||||||
|             if next(iter(val.keys())) in ["S", "B"] and next(iter(val.values())) == "" |             if next(iter(val.keys())) in ["S", "B"] and next(iter(val.values())) == "" | ||||||
|         ] |         ] | ||||||
|         return next( | 
 | ||||||
|  |         # First validate that all of the GSI-keys are set | ||||||
|  |         empty_gsi_key = next( | ||||||
|  |             (kn for kn in gsi_key_names if kn in empty_str_fields), None | ||||||
|  |         ) | ||||||
|  |         if empty_gsi_key: | ||||||
|  |             gsi_name = table.global_indexes[0].name | ||||||
|  |             raise MockValidationException( | ||||||
|  |                 f"One or more parameter values are not valid. A value specified for a secondary index key is not supported. The AttributeValue for a key attribute cannot contain an empty string value. IndexName: {gsi_name}, IndexKey: {empty_gsi_key}" | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         # Then validate that all of the regular keys are set | ||||||
|  |         empty_key = next( | ||||||
|             (keyname for keyname in key_names if keyname in empty_str_fields), None |             (keyname for keyname in key_names if keyname in empty_str_fields), None | ||||||
|         ) |         ) | ||||||
|     return None |         if empty_key: | ||||||
|  |             msg = ( | ||||||
|  |                 custom_error_msg | ||||||
|  |                 or "One or more parameter values were invalid: An AttributeValue may not contain an empty string. Key: {}" | ||||||
|  |             ) | ||||||
|  |             raise MockValidationException(msg.format(empty_key)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def put_has_empty_attrs(field_updates: Dict[str, Any], table: Table) -> bool: | def put_has_empty_attrs(field_updates: Dict[str, Any], table: Table) -> bool: | ||||||
| @ -401,11 +423,7 @@ class DynamoHandler(BaseResponse): | |||||||
|             raise MockValidationException("Return values set to invalid value") |             raise MockValidationException("Return values set to invalid value") | ||||||
| 
 | 
 | ||||||
|         table = self.dynamodb_backend.get_table(name) |         table = self.dynamodb_backend.get_table(name) | ||||||
|         empty_key = get_empty_keys_on_put(item, table) |         validate_put_has_empty_keys(item, table) | ||||||
|         if empty_key: |  | ||||||
|             raise MockValidationException( |  | ||||||
|                 f"One or more parameter values were invalid: An AttributeValue may not contain an empty string. Key: {empty_key}" |  | ||||||
|             ) |  | ||||||
|         if put_has_empty_attrs(item, table): |         if put_has_empty_attrs(item, table): | ||||||
|             raise MockValidationException( |             raise MockValidationException( | ||||||
|                 "One or more parameter values were invalid: An number set  may not be empty" |                 "One or more parameter values were invalid: An number set  may not be empty" | ||||||
| @ -462,10 +480,10 @@ class DynamoHandler(BaseResponse): | |||||||
|                 request = list(table_request.values())[0] |                 request = list(table_request.values())[0] | ||||||
|                 if request_type == "PutRequest": |                 if request_type == "PutRequest": | ||||||
|                     item = request["Item"] |                     item = request["Item"] | ||||||
|                     empty_key = get_empty_keys_on_put(item, table) |                     validate_put_has_empty_keys( | ||||||
|                     if empty_key: |                         item, | ||||||
|                         raise MockValidationException( |                         table, | ||||||
|                             f"One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: {empty_key}" |                         custom_error_msg="One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: {}", | ||||||
|                     ) |                     ) | ||||||
|                     put_requests.append((table_name, item)) |                     put_requests.append((table_name, item)) | ||||||
|                 elif request_type == "DeleteRequest": |                 elif request_type == "DeleteRequest": | ||||||
| @ -1004,6 +1022,12 @@ class DynamoHandler(BaseResponse): | |||||||
| 
 | 
 | ||||||
|     def transact_write_items(self) -> str: |     def transact_write_items(self) -> str: | ||||||
|         transact_items = self.body["TransactItems"] |         transact_items = self.body["TransactItems"] | ||||||
|  |         # Validate first - we should error before we start the transaction | ||||||
|  |         for item in transact_items: | ||||||
|  |             if "Put" in item: | ||||||
|  |                 item_attrs = item["Put"]["Item"] | ||||||
|  |                 table = self.dynamodb_backend.get_table(item["Put"]["TableName"]) | ||||||
|  |                 validate_put_has_empty_keys(item_attrs, table) | ||||||
|         self.dynamodb_backend.transact_write_items(transact_items) |         self.dynamodb_backend.transact_write_items(transact_items) | ||||||
|         response: Dict[str, Any] = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}} |         response: Dict[str, Any] = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}} | ||||||
|         return dynamo_json_dump(response) |         return dynamo_json_dump(response) | ||||||
|  | |||||||
| @ -803,6 +803,45 @@ def test_transact_write_items_multiple_operations_fail(): | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @mock_dynamodb | ||||||
|  | def test_transact_write_items_with_empty_gsi_key(): | ||||||
|  |     client = boto3.client("dynamodb", "us-east-2") | ||||||
|  | 
 | ||||||
|  |     client.create_table( | ||||||
|  |         TableName="test_table", | ||||||
|  |         KeySchema=[{"AttributeName": "unique_code", "KeyType": "HASH"}], | ||||||
|  |         AttributeDefinitions=[ | ||||||
|  |             {"AttributeName": "unique_code", "AttributeType": "S"}, | ||||||
|  |             {"AttributeName": "unique_id", "AttributeType": "S"}, | ||||||
|  |         ], | ||||||
|  |         GlobalSecondaryIndexes=[ | ||||||
|  |             { | ||||||
|  |                 "IndexName": "gsi_index", | ||||||
|  |                 "KeySchema": [{"AttributeName": "unique_id", "KeyType": "HASH"}], | ||||||
|  |                 "Projection": {"ProjectionType": "ALL"}, | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |         ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     transact_items = [ | ||||||
|  |         { | ||||||
|  |             "Put": { | ||||||
|  |                 "Item": {"unique_code": {"S": "some code"}, "unique_id": {"S": ""}}, | ||||||
|  |                 "TableName": "test_table", | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     with pytest.raises(ClientError) as exc: | ||||||
|  |         client.transact_write_items(TransactItems=transact_items) | ||||||
|  |     err = exc.value.response["Error"] | ||||||
|  |     err["Code"].should.equal("ValidationException") | ||||||
|  |     err["Message"].should.equal( | ||||||
|  |         "One or more parameter values are not valid. A value specified for a secondary index key is not supported. The AttributeValue for a key attribute cannot contain an empty string value. IndexName: gsi_index, IndexKey: unique_id" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @mock_dynamodb | @mock_dynamodb | ||||||
| def test_update_primary_key_with_sortkey(): | def test_update_primary_key_with_sortkey(): | ||||||
|     dynamodb = boto3.resource("dynamodb", region_name="us-east-1") |     dynamodb = boto3.resource("dynamodb", region_name="us-east-1") | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user