Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,16 +388,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(
Expand All @@ -415,6 +412,42 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P

return resources

def _make_lambda_role(
self,
lambda_function: LambdaFunction,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implementation has some unnecessary change IMO, we can keep the if lambda_function.Role is None part as is, then _make_lambda_role can return the created role, so we don't need to pass in lambda_function in this function.

Copy link
Member

@roger-zhangg roger-zhangg Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other concern is we are modifying values for lambda_function, execution_role, resources, conditions ( which are the function's input) inside this function which returns None, but all these values might have changed after running this function. This might make this part of the code harder to review in the future. If we need to change all these variable's value in place, I would perfer to implement this directly in to_cloudformation instead of this subfunction. If you really prefer to have this subfunction. Maybe we can return the new value instead of modifying them inplace. So future reviewers could easily understand these variables are modified in this subfunction.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not exactly correct here. In theory we need to create this role only if one of the values is AWS::NoValue.

If the Role field has an If with two different roles, then we shouldn't create the new one. (so, move this resources.append to inside a new condition "if 1 or 2 are intrinsic_no_value"


# 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,
Expand Down
112 changes: 112 additions & 0 deletions tests/model/test_sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,118 @@ def test_function_datasource_set_with_none():
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": {}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this self.template variable since it's not being used. Right below you use it to get Conditions from here, but it will be just {}, so you can just do that ("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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do the same than for the roles with the functions: generated_functions = [x for x in cfnResources if isinstance(x, LambdaFunction)]
and then you can also confirm that there's only one created.

But it's not a big deal. We can probably change it in the future if needed.


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", {})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't use the variable template anywhere else, so you could just remove it and define the conditions below. (same in other tests)

Suggested change
template = {"Conditions": {"Condition": True}}
kwargs = dict(self.kwargs)
kwargs["conditions"] = template.get("Conditions", {})
kwargs = dict(self.kwargs)
kwargs["conditions"] = {"Condition": True}


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)


class TestSamCapacityProvider(TestCase):
"""Tests for SamCapacityProvider"""

Expand Down
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions tests/translator/input/function_with_iam_role.yaml
Original file line number Diff line number Diff line change
@@ -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 <function-logical-id>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"
Original file line number Diff line number Diff line change
@@ -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."
}
]
}
85 changes: 85 additions & 0 deletions tests/translator/output/aws-cn/function_with_iam_role.json
Original file line number Diff line number Diff line change
@@ -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 <function-logical-id>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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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."
}
]
}
Loading
Loading