diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index d55a7b1efad2f..0ae8123fa292a 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -230,6 +230,14 @@ LAMBDA_DEFAULT_MEMORY_SIZE = 128 LAMBDA_TAG_LIMIT_PER_RESOURCE = 50 +LAMBDA_TAG_KEY_LEN_LIMIT = 128 +LAMBDA_TAG_VALUE_LEN_LIMIT = 256 +RESERVED_TAG_PREFIX = "aws:" +LAMBDA_DEFAULT_TAG_PREFIX = "aws:cloudformation:" +LAMBDA_DEFAULT_TAGS = frozenset( + [LAMBDA_DEFAULT_TAG_PREFIX + tag for tag in ["logical-id", "stack-id", "stack-name"]] +) +LAMBDA_TAG_ALLOWED_CHARS = re.compile(r"^[a-zA-Z0-9\s+\-=._:/@]+$") LAMBDA_LAYERS_LIMIT_PER_FUNCTION = 5 TAG_KEY_CUSTOM_URL = "_custom_id_" @@ -4022,6 +4030,23 @@ def _store_tags(self, function: Function, tags: dict[str, str]): raise InvalidParameterValueException( "Number of tags exceeds resource tag limit.", Type="User" ) + # following validation conditions are performed based on the following AWS guidelines + # https://docs.aws.amazon.com/lambda/latest/dg/configuration-tags.html + for tag_key, tag_value in tags.items(): + if len(tag_key) > 128 or len(tag_value) > 256: + raise InvalidParameterValueException( + "Length of tag's key or value exceed limit.", Type="User" + ) + if not bool(LAMBDA_TAG_ALLOWED_CHARS.match(tag_key)) or not bool( + LAMBDA_TAG_ALLOWED_CHARS.match(tag_value) + ): + raise InvalidParameterValueException( + "Tag key or value contains non allowed characters.", Type="User" + ) + if tag_key.startswith(RESERVED_TAG_PREFIX) and tag_key not in LAMBDA_DEFAULT_TAGS: + raise InvalidParameterValueException( + "Tag key cannot contain reserved prefix (aws:)", Type="User" + ) with function.lock: function.tags = tags # dirty hack for changed revision id, should reevaluate model to prevent this: diff --git a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py index 155b35f486adc..81d7adec4aebf 100644 --- a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py +++ b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py @@ -358,7 +358,20 @@ def create( kwargs["Environment"] = { "Variables": {k: str(v) for k, v in environment_variables.items()} } - + # kwargs["Tags"] = request.desired_state.get('Tags',[]) + tags = dict() + for tag_dict in request.desired_state.get("Tags", []): + tag_dict_key = tag_dict.get("Key") + tag_dict_value = tag_dict.get("Value") + tags[tag_dict_key] = tag_dict_value + + # the following mmust be added by default as specified here: + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html + tags["aws:cloudformation:logical-id"] = request.logical_resource_id + tags["aws:cloudformation:stack-id"] = request.stack_id + tags["aws:cloudformation:stack-name"] = request.stack_name + + kwargs["Tags"] = tags kwargs["Code"] = _get_lambda_code_param(model) create_response = lambda_client.create_function(**kwargs) model["Arn"] = create_response["FunctionArn"] diff --git a/tests/aws/services/lambda_/test_lambda_api.py b/tests/aws/services/lambda_/test_lambda_api.py index df6e1bfdb23c4..65ea708d6d48a 100644 --- a/tests/aws/services/lambda_/test_lambda_api.py +++ b/tests/aws/services/lambda_/test_lambda_api.py @@ -5353,6 +5353,108 @@ def test_tag_limits(self, create_lambda_function, snapshot, aws_client): aws_client.lambda_.tag_resource(Resource=function_arn, Tags={"a_key": "a_value"}) snapshot.match("tag_lambda_too_many_tags_additional", e.value.response) + @markers.aws.validated + def test_tag_invalid(self, create_lambda_function, snapshot, aws_client): + """""" + function_name = f"fn-tag-{short_uid()}" + create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + ) + function_arn = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ + "FunctionArn" + ] + + # invalid + long_tag_key = 'k' * 129 + tags = {long_tag_key: 'value', 'normal_key': 'normal_value'} + + with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: + aws_client.lambda_.tag_resource( + Resource=function_arn, Tags=tags + ) + + snapshot.match("tag_lambda_invalid_key_length", e.value.response) + + # invalid + long_tag_value = 'v' * 257 + tags = {'key': long_tag_value, 'normal_key': 'normal_value'} + + with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: + aws_client.lambda_.tag_resource( + Resource=function_arn, Tags=tags + ) + + snapshot.match("tag_lambda_invalid_value_length", e.value.response) + + # Test invalid characters in tag key + invalid_tag_key = 'invalid*key' + tags = {invalid_tag_key: 'value', 'normal_key': 'normal_value'} + + with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: + aws_client.lambda_.tag_resource( + Resource=function_arn, Tags=tags + ) + + snapshot.match("tag_lambda_invalid_chars_in_key", e.value.response) + + # Test invalid character in tag value + invalid_tag_value = 'invalid*value' + tags = {'key': invalid_tag_value, 'normal_key': 'normal_value'} + + with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: + aws_client.lambda_.tag_resource( + Resource=function_arn, Tags=tags + ) + + snapshot.match("tag_lambda_invalid_chars_in_value", e.value.response) + + # Test reserved tag prefix (aws:) not allowed + reserved_tag_key = 'aws:some_key' + tags = {reserved_tag_key: 'value'} + + with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: + aws_client.lambda_.tag_resource( + Resource=function_arn, Tags=tags + ) + + snapshot.match("tag_lambda_invalid_prefix_in_key", e.value.response) + + # Test valid tags are accepted + valid_tags = {'validKey': 'validValue', 'anotherKey': 'anotherValue'} + aws_client.lambda_.tag_resource(Resource=function_arn, Tags=valid_tags) + # Confirm that the valid tags are applied + + response = aws_client.lambda_.list_tags(Resource=function_arn) + + snapshot.match("tag_lambda_valid_tags", response) + + + # @markers.aws.validated + # def test_tag_value_length_invalid(self, create_lambda_function, snapshot, aws_client): + # """""" + # function_name = f"fn-tag-{short_uid()}" + # create_lambda_function( + # handler_file=TEST_LAMBDA_PYTHON_ECHO, + # func_name=function_name, + # runtime=Runtime.python3_12, + # ) + # function_arn = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ + # "FunctionArn" + # ] + # + # # invalid + # long_tag_value = 'v' * 257 + # tags = {'key': long_tag_value} + # + # with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: + # aws_client.lambda_.tag_resource( + # Resource=function_arn, Tags=tags + # ) + # + # snapshot.match("tag_lambda_invalid_value_length", e.value.response) + @markers.aws.validated def test_tag_versions(self, create_lambda_function, snapshot, aws_client): function_name = f"fn-tag-{short_uid()}" diff --git a/tests/aws/services/lambda_/test_lambda_api.snapshot.json b/tests/aws/services/lambda_/test_lambda_api.snapshot.json index faf8736857400..fceeb40919b5e 100644 --- a/tests/aws/services/lambda_/test_lambda_api.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_api.snapshot.json @@ -17462,5 +17462,80 @@ } } } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_invalid": { + "recorded-date": "18-09-2024, 08:13:12", + "recorded-content": { + "tag_lambda_invalid_key_length": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Length of tag's key or value exceed limit." + }, + "Type": "User", + "message": "Length of tag's key or value exceed limit.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "tag_lambda_invalid_value_length": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Length of tag's key or value exceed limit." + }, + "Type": "User", + "message": "Length of tag's key or value exceed limit.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "tag_lambda_invalid_chars_in_key": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Tag key or value contains non allowed characters." + }, + "Type": "User", + "message": "Tag key or value contains non allowed characters.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "tag_lambda_invalid_chars_in_value": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Tag key or value contains non allowed characters." + }, + "Type": "User", + "message": "Tag key or value contains non allowed characters.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "tag_lambda_invalid_prefix_in_key": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Tag key cannot contain reserved prefix (aws:)" + }, + "Type": "User", + "message": "Tag key cannot contain reserved prefix (aws:)", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "tag_lambda_valid_tags": { + "Tags": { + "anotherKey": "anotherValue", + "validKey": "validValue" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } }