Skip to content
Merged
63 changes: 53 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, conditions)

try:
resources += self._generate_event_resources(
Expand All @@ -415,6 +412,52 @@ 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],
conditions: Dict[str, 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")

elif is_intrinsic_if(lambda_role):

# We need to create and if else condition here
role_resolved_value = intrinsics_resolver.resolve_parameter_refs(lambda_role)
role_condition, role_if, role_else = role_resolved_value.get("Fn::If")

is_both_intrinsic_no_values = is_intrinsic_no_value(role_if) and is_intrinsic_no_value(role_else)

# Create a role only when no value is in either of the conditions
if is_intrinsic_no_value(role_if) or is_intrinsic_no_value(role_else):
resources.append(execution_role)

# both are none values, we always need to create a role
if is_both_intrinsic_no_values:
lambda_function.Role = execution_role.get_runtime_attr("arn")

# first value is none so we should create condition ? create : [2]
# create a condition for IAM role to only create on if case
elif is_intrinsic_no_value(role_if):
lambda_function.Role = make_conditional(
role_condition, execution_role.get_runtime_attr("arn"), role_else
)
execution_role.set_resource_attribute("Condition", f"{role_condition}")

# second value is none so we should create condition ? [1] : create
# create a condition for IAM role to only create on else case
# with top level condition that negates the condition passed
elif is_intrinsic_no_value(role_else):
lambda_function.Role = make_conditional(role_condition, role_if, execution_role.get_runtime_attr("arn"))
execution_role.set_resource_attribute("Condition", f"NOT{role_condition}")
conditions[f"NOT{role_condition}"] = make_not_conditional(role_condition)

def _construct_event_invoke_config( # noqa: PLR0913
self,
function_name: str,
Expand Down
113 changes: 113 additions & 0 deletions tests/model/test_sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,119 @@ 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": {}}

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")

# Should not create a role if a role is passed in for both cases
self.assertEqual(len(generated_roles), 0)
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
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:
CreateRole: !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
- CreateRole
- !Ref "AWS::NoValue"
- !Ref "iamRoleArn"
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."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"Conditions": {
"CreateRole": {
"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": [
"CreateRole",
{
"Fn::GetAtt": [
"MinimalFunctionRole",
"Arn"
]
},
{
"Ref": "iamRoleArn"
}
]
},
"Runtime": "python3.10",
"Tags": [
{
"Key": "lambda:createdBy",
"Value": "SAM"
}
]
},
"Type": "AWS::Lambda::Function"
},
"MinimalFunctionRole": {
"Condition": "CreateRole",
"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"
}
}
}
Loading
Loading