diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index dca5a2687..ac1c37f39 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -347,8 +347,9 @@ def parse_and_update_resource(logical_id, resource_json, resources_map, region_n return None -def parse_and_delete_resource(resource_name, resource_json, resources_map, region_name): - resource_class, resource_json, _ = parse_resource(resource_json, resources_map) +def parse_and_delete_resource(resource_name, resource_json, region_name): + resource_type = resource_json["Type"] + resource_class = resource_class_from_type(resource_type) if not hasattr( resource_class.delete_from_cloudformation_json, "__isabstractmethod__" ): @@ -663,9 +664,7 @@ class ResourceMap(collections_abc.Mapping): ].physical_resource_id else: resource_name = None - parse_and_delete_resource( - resource_name, resource_json, self, self._region_name - ) + parse_and_delete_resource(resource_name, resource_json, self._region_name) self._parsed_resources.pop(logical_name) self._template = template @@ -726,7 +725,7 @@ class ResourceMap(collections_abc.Mapping): ] parse_and_delete_resource( - resource_name, resource_json, self, self._region_name, + resource_name, resource_json, self._region_name, ) self._parsed_resources.pop(parsed_resource.logical_resource_id) diff --git a/moto/iam/models.py b/moto/iam/models.py index 3b31fb338..d6fd69ed1 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -598,6 +598,19 @@ class Role(CloudFormationModel): return role + @classmethod + def delete_from_cloudformation_json( + cls, resource_name, cloudformation_json, region_name + ): + for profile_name, profile in iam_backend.instance_profiles.items(): + profile.delete_role(role_name=resource_name) + + for _, role in iam_backend.roles.items(): + if role.name == resource_name: + for arn, policy in role.policies.items(): + role.delete_policy(arn) + iam_backend.delete_role(resource_name) + @property def arn(self): return "arn:aws:iam::{0}:role{1}{2}".format(ACCOUNT_ID, self.path, self.name) @@ -678,7 +691,7 @@ class Role(CloudFormationModel): @property def physical_resource_id(self): - return self.id + return self.name def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -725,13 +738,22 @@ class InstanceProfile(CloudFormationModel): ): properties = cloudformation_json["Properties"] - role_ids = properties["Roles"] + role_names = properties["Roles"] return iam_backend.create_instance_profile( name=resource_physical_name, path=properties.get("Path", "/"), - role_ids=role_ids, + role_names=role_names, ) + @classmethod + def delete_from_cloudformation_json( + cls, resource_name, cloudformation_json, region_name + ): + iam_backend.delete_instance_profile(resource_name) + + def delete_role(self, role_name): + self.roles = [role for role in self.roles if role.name != role_name] + @property def arn(self): return "arn:aws:iam::{0}:instance-profile{1}{2}".format( @@ -1878,7 +1900,7 @@ class IAMBackend(BaseBackend): return raise IAMNotFoundException("Policy not found") - def create_instance_profile(self, name, path, role_ids, tags=None): + def create_instance_profile(self, name, path, role_names, tags=None): if self.instance_profiles.get(name): raise IAMConflictException( code="EntityAlreadyExists", @@ -1887,7 +1909,7 @@ class IAMBackend(BaseBackend): instance_profile_id = random_resource_id() - roles = [iam_backend.get_role_by_id(role_id) for role_id in role_ids] + roles = [iam_backend.get_role(role_name) for role_name in role_names] instance_profile = InstanceProfile(instance_profile_id, name, path, roles, tags) self.instance_profiles[name] = instance_profile return instance_profile diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 3ca26bfb2..f292fd024 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -324,7 +324,7 @@ class IamResponse(BaseResponse): tags = self._get_multi_param("Tags.member") profile = iam_backend.create_instance_profile( - profile_name, path, role_ids=[], tags=tags + profile_name, path, role_names=[], tags=tags ) template = self.response_template(CREATE_INSTANCE_PROFILE_TEMPLATE) return template.render(profile=profile) diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 2b2c4c780..bd0983b41 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -948,6 +948,7 @@ def test_iam_roles(): "roles" ] role_name_to_id = {} + role_names = [] for role_result in role_results: role = iam_conn.get_role(role_result.role_name) # Role name is not specified, so randomly generated - can't check exact name @@ -958,6 +959,7 @@ def test_iam_roles(): role_name_to_id["no-path"] = role.role_id role.role_name.should.equal("my-role-no-path-name") role.path.should.equal("/") + role_names.append(role.role_name) instance_profile_responses = iam_conn.list_instance_profiles()[ "list_instance_profiles_response" @@ -997,9 +999,7 @@ def test_iam_roles(): role_resources = [ resource for resource in resources if resource.resource_type == "AWS::IAM::Role" ] - {r.physical_resource_id for r in role_resources}.should.equal( - set(role_name_to_id.values()) - ) + {r.physical_resource_id for r in role_resources}.should.equal(set(role_names)) @mock_ec2_deprecated() diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py index 0196eb07d..268a55585 100644 --- a/tests/test_iam/test_iam_cloudformation.py +++ b/tests/test_iam/test_iam_cloudformation.py @@ -8,6 +8,58 @@ from botocore.exceptions import ClientError from moto import mock_iam, mock_cloudformation, mock_s3, mock_sts from moto.core import ACCOUNT_ID + +TEMPLATE_MINIMAL_ROLE = """ +AWSTemplateFormatVersion: 2010-09-09 +Resources: + RootRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' +""" + + +TEMPLATE_ROLE_INSTANCE_PROFILE = """ +AWSTemplateFormatVersion: 2010-09-09 +Resources: + RootRole: + Type: 'AWS::IAM::Role' + Properties: + RoleName: {0} + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: / + Policies: + - PolicyName: root + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: '*' + Resource: '*' + RootInstanceProfile: + Type: 'AWS::IAM::InstanceProfile' + Properties: + Path: / + Roles: + - !Ref RootRole +""" + # AWS::IAM::User Tests @mock_iam @mock_cloudformation @@ -1384,3 +1436,62 @@ Resources: access_keys = iam_client.list_access_keys(UserName=other_user_name) access_key_id.should_not.equal(access_keys["AccessKeyMetadata"][0]["AccessKeyId"]) + + +@mock_iam +@mock_cloudformation +def test_iam_cloudformation_create_role(): + cf_client = boto3.client("cloudformation", region_name="us-east-1") + + stack_name = "MyStack" + + template = TEMPLATE_MINIMAL_ROLE.strip() + cf_client.create_stack(StackName=stack_name, TemplateBody=template) + + resources = cf_client.list_stack_resources(StackName=stack_name)[ + "StackResourceSummaries" + ] + role = [res for res in resources if res["ResourceType"] == "AWS::IAM::Role"][0] + role["LogicalResourceId"].should.equal("RootRole") + role_name = role["PhysicalResourceId"] + + iam_client = boto3.client("iam", region_name="us-east-1") + iam_client.list_roles()["Roles"].should.have.length_of(1) + + cf_client.delete_stack(StackName=stack_name) + + iam_client.list_roles()["Roles"].should.have.length_of(0) + + +@mock_iam +@mock_cloudformation +def test_iam_cloudformation_create_role_and_instance_profile(): + cf_client = boto3.client("cloudformation", region_name="us-east-1") + + stack_name = "MyStack" + role_name = "MyUser" + + template = TEMPLATE_ROLE_INSTANCE_PROFILE.strip().format(role_name) + cf_client.create_stack(StackName=stack_name, TemplateBody=template) + + resources = cf_client.list_stack_resources(StackName=stack_name)[ + "StackResourceSummaries" + ] + role = [res for res in resources if res["ResourceType"] == "AWS::IAM::Role"][0] + role["LogicalResourceId"].should.equal("RootRole") + role["PhysicalResourceId"].should.equal(role_name) + profile = [ + res for res in resources if res["ResourceType"] == "AWS::IAM::InstanceProfile" + ][0] + profile["LogicalResourceId"].should.equal("RootInstanceProfile") + profile["PhysicalResourceId"].should.contain( + stack_name + ) # e.g. MyStack-RootInstanceProfile-73Y4H4ALFW3N + profile["PhysicalResourceId"].should.contain("RootInstanceProfile") + + iam_client = boto3.client("iam", region_name="us-east-1") + iam_client.list_roles()["Roles"].should.have.length_of(1) + + cf_client.delete_stack(StackName=stack_name) + + iam_client.list_roles()["Roles"].should.have.length_of(0)