From ef9cd2206c4c9e306168eed0fc53adb279460589 Mon Sep 17 00:00:00 2001 From: Sid Madipalli Date: Mon, 10 Nov 2025 14:08:20 -0800 Subject: [PATCH 1/2] Adding AWS::NoValue support to Serverless Function IAM role --- samtranslator/model/sam_resources.py | 53 ++++++++++--- tests/model/test_sam_resources.py | 112 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 10 deletions(-) diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 70e7ef6750..b246e4afd4 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -347,16 +347,13 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P managed_policy_map = kwargs.get("managed_policy_map", {}) get_managed_policy_map = kwargs.get("get_managed_policy_map") - execution_role = None - if lambda_function.Role is None: - execution_role = self._construct_role( - managed_policy_map, - event_invoke_policies, - intrinsics_resolver, - get_managed_policy_map, - ) - lambda_function.Role = execution_role.get_runtime_attr("arn") - resources.append(execution_role) + execution_role = self._construct_role( + managed_policy_map, + event_invoke_policies, + intrinsics_resolver, + get_managed_policy_map, + ) + self._make_lambda_role(lambda_function, intrinsics_resolver, execution_role, resources) try: resources += self._generate_event_resources( @@ -374,6 +371,42 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P return resources + def _make_lambda_role( + self, + lambda_function: LambdaFunction, + intrinsics_resolver: IntrinsicsResolver, + execution_role: IAMRole, + resources: List[Any], + ) -> None: + lambda_role = lambda_function.Role + + if lambda_role is None: + resources.append(execution_role) + lambda_function.Role = execution_role.get_runtime_attr("arn") + + if is_intrinsic_if(lambda_role): + resources.append(execution_role) + + # We need to create and if else condition here + role_resolved_value = intrinsics_resolver.resolve_parameter_refs(self.Role) + role_list = role_resolved_value.get("Fn::If") + + # both are none values then we need to create a role + if is_intrinsic_no_value(role_list[1]) and is_intrinsic_no_value(role_list[2]): + lambda_function.Role = execution_role.get_runtime_attr("arn") + + # first value is none so we should create condition ? create : [2] + elif is_intrinsic_no_value(role_list[1]): + lambda_function.Role = make_conditional( + role_list[0], execution_role.get_runtime_attr("arn"), role_list[2] + ) + + # second value is none so we should create condition ? [1] : create + elif is_intrinsic_no_value(role_list[2]): + lambda_function.Role = make_conditional( + role_list[0], role_list[1], execution_role.get_runtime_attr("arn") + ) + def _construct_event_invoke_config( # noqa: PLR0913 self, function_name: str, diff --git a/tests/model/test_sam_resources.py b/tests/model/test_sam_resources.py index b53649f107..5db3868449 100644 --- a/tests/model/test_sam_resources.py +++ b/tests/model/test_sam_resources.py @@ -738,3 +738,115 @@ def test_function_datasource_set_with_none(): api = SamGraphQLApi("MyApi") none_datasource = api._construct_none_datasource("foo") assert none_datasource + + +class TestSamFunctionRoleResolver(TestCase): + """ + Tests for resolving IAM role property values in SamFunction + """ + + def setUp(self): + self.function = SamFunction("foo") + self.function.CodeUri = "s3://foobar/foo.zip" + self.function.Runtime = "foo" + self.function.Handler = "bar" + self.template = {"Conditions": {}} + + self.kwargs = { + "intrinsics_resolver": IntrinsicsResolver({}), + "event_resources": [], + "managed_policy_map": {}, + "resource_resolver": ResourceResolver({}), + "conditions": self.template.get("Conditions", {}), + } + + def test_role_none_creates_execution_role(self): + self.function.Role = None + cfn_resources = self.function.to_cloudformation(**self.kwargs) + generated_roles = [x for x in cfn_resources if isinstance(x, IAMRole)] + + self.assertEqual(len(generated_roles), 1) # Should create execution role + + def test_role_explicit_arn_no_execution_role(self): + test_role = "arn:aws:iam::123456789012:role/existing-role" + self.function.Role = test_role + + cfn_resources = self.function.to_cloudformation(**self.kwargs) + generated_roles = [x for x in cfn_resources if isinstance(x, IAMRole)] + lambda_function = next(r for r in cfn_resources if r.resource_type == "AWS::Lambda::Function") + + self.assertEqual(len(generated_roles), 0) # Should not create execution role + self.assertEqual(lambda_function.Role, test_role) + + def test_role_fn_if_no_aws_no_value_keeps_original(self): + role_conditional = { + "Fn::If": ["Condition", "arn:aws:iam::123456789012:role/existing-role", {"Ref": "iamRoleArn"}] + } + self.function.Role = role_conditional + + template = {"Conditions": {"Condition": True}} + kwargs = dict(self.kwargs) + kwargs["conditions"] = template.get("Conditions", {}) + + cfn_resources = self.function.to_cloudformation(**self.kwargs) + generated_roles = [x for x in cfn_resources if isinstance(x, IAMRole)] + lambda_function = next(r for r in cfn_resources if r.resource_type == "AWS::Lambda::Function") + + self.assertEqual(len(generated_roles), 1) + self.assertEqual(lambda_function.Role, role_conditional) + + def test_role_fn_if_both_no_value_creates_execution_role(self): + role_conditional = {"Fn::If": ["Condition", {"Ref": "AWS::NoValue"}, {"Ref": "AWS::NoValue"}]} + self.function.Role = role_conditional + + template = {"Conditions": {"Condition": True}} + kwargs = dict(self.kwargs) + kwargs["conditions"] = template.get("Conditions", {}) + + cfn_resources = self.function.to_cloudformation(**self.kwargs) + generated_roles = [x for x in cfn_resources if isinstance(x, IAMRole)] + + self.assertEqual(len(generated_roles), 1) + + def test_role_fn_if_first_no_value_creates_conditional_role(self): + role_conditional = {"Fn::If": ["Condition", {"Ref": "AWS::NoValue"}, {"Ref": "iamRoleArn"}]} + self.function.Role = role_conditional + + template = {"Conditions": {"Condition": True}} + kwargs = dict(self.kwargs) + kwargs["conditions"] = template.get("Conditions", {}) + + cfn_resources = self.function.to_cloudformation(**self.kwargs) + generated_roles = [x for x in cfn_resources if isinstance(x, IAMRole)] + lambda_function = next(r for r in cfn_resources if r.resource_type == "AWS::Lambda::Function") + + self.assertEqual(len(generated_roles), 1) + self.assertEqual( + lambda_function.Role, {"Fn::If": ["Condition", {"Fn::GetAtt": ["fooRole", "Arn"]}, {"Ref": "iamRoleArn"}]} + ) + + def test_role_fn_if_second_no_value_creates_conditional_role(self): + role_conditional = {"Fn::If": ["Condition", {"Ref": "iamRoleArn"}, {"Ref": "AWS::NoValue"}]} + self.function.Role = role_conditional + + template = {"Conditions": {"Condition": True}} + kwargs = dict(self.kwargs) + kwargs["conditions"] = template.get("Conditions", {}) + + cfn_resources = self.function.to_cloudformation(**self.kwargs) + generated_roles = [x for x in cfn_resources if isinstance(x, IAMRole)] + lambda_function = next(r for r in cfn_resources if r.resource_type == "AWS::Lambda::Function") + + self.assertEqual(len(generated_roles), 1) + self.assertEqual( + lambda_function.Role, {"Fn::If": ["Condition", {"Ref": "iamRoleArn"}, {"Fn::GetAtt": ["fooRole", "Arn"]}]} + ) + + def test_role_get_att_no_execution_role(self): + role_get_att = {"Fn::GetAtt": ["MyCustomRole", "Arn"]} + self.function.Role = role_get_att + + cfn_resources = self.function.to_cloudformation(**self.kwargs) + lambda_function = next(r for r in cfn_resources if r.resource_type == "AWS::Lambda::Function") + + self.assertEqual(lambda_function.Role, role_get_att) From 79e868011d0e442a32c697366cfea0bc45a2ffd5 Mon Sep 17 00:00:00 2001 From: Sid Madipalli Date: Mon, 8 Dec 2025 14:14:26 -0800 Subject: [PATCH 2/2] Added translator transform templated tests --- tests/model/test_sam_resources.py | 2 + .../error_function_invalid_iam_role_type.yaml | 8 ++ .../input/function_with_iam_role.yaml | 20 +++++ .../error_function_invalid_iam_role_type.json | 14 +++ .../output/aws-cn/function_with_iam_role.json | 85 +++++++++++++++++++ .../error_function_invalid_iam_role_type.json | 14 +++ .../aws-us-gov/function_with_iam_role.json | 85 +++++++++++++++++++ .../error_function_invalid_iam_role_type.json | 14 +++ .../output/function_with_iam_role.json | 85 +++++++++++++++++++ 9 files changed, 327 insertions(+) create mode 100644 tests/translator/input/error_function_invalid_iam_role_type.yaml create mode 100644 tests/translator/input/function_with_iam_role.yaml create mode 100644 tests/translator/output/aws-cn/error_function_invalid_iam_role_type.json create mode 100644 tests/translator/output/aws-cn/function_with_iam_role.json create mode 100644 tests/translator/output/aws-us-gov/error_function_invalid_iam_role_type.json create mode 100644 tests/translator/output/aws-us-gov/function_with_iam_role.json create mode 100644 tests/translator/output/error_function_invalid_iam_role_type.json create mode 100644 tests/translator/output/function_with_iam_role.json diff --git a/tests/model/test_sam_resources.py b/tests/model/test_sam_resources.py index 4c66ec4211..edc54004ae 100644 --- a/tests/model/test_sam_resources.py +++ b/tests/model/test_sam_resources.py @@ -851,6 +851,8 @@ def test_role_get_att_no_execution_role(self): lambda_function = next(r for r in cfn_resources if r.resource_type == "AWS::Lambda::Function") self.assertEqual(lambda_function.Role, role_get_att) + + class TestSamCapacityProvider(TestCase): """Tests for SamCapacityProvider""" diff --git a/tests/translator/input/error_function_invalid_iam_role_type.yaml b/tests/translator/input/error_function_invalid_iam_role_type.yaml new file mode 100644 index 0000000000..abcac61df9 --- /dev/null +++ b/tests/translator/input/error_function_invalid_iam_role_type.yaml @@ -0,0 +1,8 @@ +Resources: + MinimalFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.10 + Role: 2 diff --git a/tests/translator/input/function_with_iam_role.yaml b/tests/translator/input/function_with_iam_role.yaml new file mode 100644 index 0000000000..96729196a3 --- /dev/null +++ b/tests/translator/input/function_with_iam_role.yaml @@ -0,0 +1,20 @@ +Parameters: + iamRoleArn: + Type: String + Description: The ARN of an IAM role to use as this function's execution role. + If a role isn't specified, one is created for you with a logical ID of Role. + +Conditions: + RoleExists: !Not [!Equals ['', !Ref iamRoleArn]] + +Resources: + MinimalFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.10 + Role: !If + - RoleExists + - !Ref "iamRoleArn" + - !Ref "AWS::NoValue" diff --git a/tests/translator/output/aws-cn/error_function_invalid_iam_role_type.json b/tests/translator/output/aws-cn/error_function_invalid_iam_role_type.json new file mode 100644 index 0000000000..22f3b61ecf --- /dev/null +++ b/tests/translator/output/aws-cn/error_function_invalid_iam_role_type.json @@ -0,0 +1,14 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 1. ", + "Resource with id [MinimalFunction] is invalid. ", + "Property 'Role' should be a string." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MinimalFunction] is invalid. Property 'Role' should be a string.", + "errors": [ + { + "errorMessage": "Resource with id [MinimalFunction] is invalid. Property 'Role' should be a string." + } + ] +} diff --git a/tests/translator/output/aws-cn/function_with_iam_role.json b/tests/translator/output/aws-cn/function_with_iam_role.json new file mode 100644 index 0000000000..d6f9ed00a3 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_iam_role.json @@ -0,0 +1,85 @@ +{ + "Conditions": { + "RoleExists": { + "Fn::Not": [ + { + "Fn::Equals": [ + "", + { + "Ref": "iamRoleArn" + } + ] + } + ] + } + }, + "Parameters": { + "iamRoleArn": { + "Description": "The ARN of an IAM role to use as this function's execution role. If a role isn't specified, one is created for you with a logical ID of Role.", + "Type": "String" + } + }, + "Resources": { + "MinimalFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::If": [ + "RoleExists", + { + "Ref": "iamRoleArn" + }, + { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + } + ] + }, + "Runtime": "python3.10", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MinimalFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/aws-us-gov/error_function_invalid_iam_role_type.json b/tests/translator/output/aws-us-gov/error_function_invalid_iam_role_type.json new file mode 100644 index 0000000000..22f3b61ecf --- /dev/null +++ b/tests/translator/output/aws-us-gov/error_function_invalid_iam_role_type.json @@ -0,0 +1,14 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 1. ", + "Resource with id [MinimalFunction] is invalid. ", + "Property 'Role' should be a string." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MinimalFunction] is invalid. Property 'Role' should be a string.", + "errors": [ + { + "errorMessage": "Resource with id [MinimalFunction] is invalid. Property 'Role' should be a string." + } + ] +} diff --git a/tests/translator/output/aws-us-gov/function_with_iam_role.json b/tests/translator/output/aws-us-gov/function_with_iam_role.json new file mode 100644 index 0000000000..0674fa36b2 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_iam_role.json @@ -0,0 +1,85 @@ +{ + "Conditions": { + "RoleExists": { + "Fn::Not": [ + { + "Fn::Equals": [ + "", + { + "Ref": "iamRoleArn" + } + ] + } + ] + } + }, + "Parameters": { + "iamRoleArn": { + "Description": "The ARN of an IAM role to use as this function's execution role. If a role isn't specified, one is created for you with a logical ID of Role.", + "Type": "String" + } + }, + "Resources": { + "MinimalFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::If": [ + "RoleExists", + { + "Ref": "iamRoleArn" + }, + { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + } + ] + }, + "Runtime": "python3.10", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MinimalFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/error_function_invalid_iam_role_type.json b/tests/translator/output/error_function_invalid_iam_role_type.json new file mode 100644 index 0000000000..22f3b61ecf --- /dev/null +++ b/tests/translator/output/error_function_invalid_iam_role_type.json @@ -0,0 +1,14 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 1. ", + "Resource with id [MinimalFunction] is invalid. ", + "Property 'Role' should be a string." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MinimalFunction] is invalid. Property 'Role' should be a string.", + "errors": [ + { + "errorMessage": "Resource with id [MinimalFunction] is invalid. Property 'Role' should be a string." + } + ] +} diff --git a/tests/translator/output/function_with_iam_role.json b/tests/translator/output/function_with_iam_role.json new file mode 100644 index 0000000000..3abc1c5ccc --- /dev/null +++ b/tests/translator/output/function_with_iam_role.json @@ -0,0 +1,85 @@ +{ + "Conditions": { + "RoleExists": { + "Fn::Not": [ + { + "Fn::Equals": [ + "", + { + "Ref": "iamRoleArn" + } + ] + } + ] + } + }, + "Parameters": { + "iamRoleArn": { + "Description": "The ARN of an IAM role to use as this function's execution role. If a role isn't specified, one is created for you with a logical ID of Role.", + "Type": "String" + } + }, + "Resources": { + "MinimalFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::If": [ + "RoleExists", + { + "Ref": "iamRoleArn" + }, + { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + } + ] + }, + "Runtime": "python3.10", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MinimalFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +}