From a681723a1bfaf4883d23c37e37829ba52df3a56b Mon Sep 17 00:00:00 2001 From: CodePeters Date: Tue, 17 Sep 2024 17:59:07 +0300 Subject: [PATCH 1/5] resolved tags bug --- .../localstack/services/lambda_/provider.py | 24 ++++++++++++++++++- .../resource_providers/aws_lambda_function.py | 15 +++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index d55a7b1efad2f..36c19b478143b 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -138,6 +138,7 @@ from localstack.aws.api.lambda_ import ServiceException as LambdaServiceException from localstack.aws.connect import connect_to from localstack.aws.spec import load_service +from localstack.services.cloudformation.stores import get_cloudformation_store from localstack.services.edge import ROUTER from localstack.services.lambda_ import api_utils from localstack.services.lambda_ import hooks as lambda_hooks @@ -230,6 +231,12 @@ 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 +4029,21 @@ 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: @@ -4071,7 +4093,7 @@ def list_tags( account_id, region = api_utils.get_account_and_region(resource, context) function_name = api_utils.get_function_name(resource, context) fn = self._get_function(function_name=function_name, account_id=account_id, region=region) - + state = get_cloudformation_store(context.account_id, context.region) return ListTagsResponse(Tags=self._get_tags(fn)) def untag_resource( 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..62377e0606199 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"] From b9e3f2240f330c013af1e09064a651dd9cba0447 Mon Sep 17 00:00:00 2001 From: CodePeters Date: Tue, 17 Sep 2024 17:59:07 +0300 Subject: [PATCH 2/5] removed unused line --- localstack-core/localstack/services/lambda_/provider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index 36c19b478143b..e394ee4358a79 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -138,7 +138,6 @@ from localstack.aws.api.lambda_ import ServiceException as LambdaServiceException from localstack.aws.connect import connect_to from localstack.aws.spec import load_service -from localstack.services.cloudformation.stores import get_cloudformation_store from localstack.services.edge import ROUTER from localstack.services.lambda_ import api_utils from localstack.services.lambda_ import hooks as lambda_hooks @@ -4093,7 +4092,6 @@ def list_tags( account_id, region = api_utils.get_account_and_region(resource, context) function_name = api_utils.get_function_name(resource, context) fn = self._get_function(function_name=function_name, account_id=account_id, region=region) - state = get_cloudformation_store(context.account_id, context.region) return ListTagsResponse(Tags=self._get_tags(fn)) def untag_resource( From b0c76c96e636902421230b23dc2a641f9f53a79c Mon Sep 17 00:00:00 2001 From: CodePeters Date: Tue, 17 Sep 2024 17:59:07 +0300 Subject: [PATCH 3/5] minor change --- localstack-core/localstack/services/lambda_/provider.py | 1 + 1 file changed, 1 insertion(+) diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index e394ee4358a79..a0ec9a6da302c 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -4092,6 +4092,7 @@ def list_tags( account_id, region = api_utils.get_account_and_region(resource, context) function_name = api_utils.get_function_name(resource, context) fn = self._get_function(function_name=function_name, account_id=account_id, region=region) + return ListTagsResponse(Tags=self._get_tags(fn)) def untag_resource( From 334ad5fc95930cc1433bc4e14766aef89e66609b Mon Sep 17 00:00:00 2001 From: CodePeters Date: Tue, 17 Sep 2024 17:59:07 +0300 Subject: [PATCH 4/5] fixed linting --- .../localstack/services/lambda_/provider.py | 12 ++++++++---- .../resource_providers/aws_lambda_function.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index a0ec9a6da302c..0ae8123fa292a 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -234,8 +234,10 @@ 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_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_" @@ -4031,11 +4033,13 @@ def _store_tags(self, function: Function, tags: dict[str, str]): # 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: + 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)): + 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" ) 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 62377e0606199..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 @@ -360,7 +360,7 @@ def create( } # kwargs["Tags"] = request.desired_state.get('Tags',[]) tags = dict() - for tag_dict in request.desired_state.get('Tags',[]): + 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 @@ -369,7 +369,7 @@ def create( # 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 + tags["aws:cloudformation:stack-name"] = request.stack_name kwargs["Tags"] = tags kwargs["Code"] = _get_lambda_code_param(model) From b5fab7c0d3a07576db33517b9437266120c17f4f Mon Sep 17 00:00:00 2001 From: CodePeters Date: Wed, 18 Sep 2024 11:15:43 +0300 Subject: [PATCH 5/5] included some testing for newly added tag validation conditions --- tests/aws/services/lambda_/test_lambda_api.py | 102 ++++++++++++++++++ .../lambda_/test_lambda_api.snapshot.json | 75 +++++++++++++ 2 files changed, 177 insertions(+) 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 + } + } + } } }