Skip to content

Commit

Permalink
IAM - Delete Role/InstanceProfile via CloudFormation (#3591)
Browse files Browse the repository at this point in the history
  • Loading branch information
bblommers committed Aug 28, 2021
1 parent 027d05e commit 1a42b33
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 15 deletions.
11 changes: 5 additions & 6 deletions moto/cloudformation/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__"
):
Expand Down Expand Up @@ -663,9 +664,7 @@ def update(self, template, parameters=None):
].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
Expand Down Expand Up @@ -726,7 +725,7 @@ def delete(self):
]

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)
Expand Down
32 changes: 27 additions & 5 deletions moto/iam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,19 @@ def create_from_cloudformation_json(

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)
Expand Down Expand Up @@ -678,7 +691,7 @@ def delete_policy(self, policy_name):

@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
Expand Down Expand Up @@ -725,13 +738,22 @@ def create_from_cloudformation_json(
):
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(
Expand Down Expand Up @@ -1878,7 +1900,7 @@ def delete_policy_version(self, policy_arn, version_id):
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",
Expand All @@ -1887,7 +1909,7 @@ def create_instance_profile(self, name, path, role_ids, tags=None):

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
Expand Down
2 changes: 1 addition & 1 deletion moto/iam/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def create_instance_profile(self):
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
111 changes: 111 additions & 0 deletions tests/test_iam/test_iam_cloudformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,58 @@
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
Expand Down Expand Up @@ -1384,3 +1436,62 @@ def test_iam_cloudformation_update_users_access_key_replacement():

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)

0 comments on commit 1a42b33

Please sign in to comment.