IAM - Implement ServiceLinkedRoles (#5089)

This commit is contained in:
Bert Blommers 2022-05-03 09:44:47 +00:00 committed by GitHub
parent 578de3d47f
commit 76fe578d95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 250 additions and 86 deletions

View File

@ -2903,7 +2903,7 @@
## iam
<details>
<summary>71% implemented</summary>
<summary>73% implemented</summary>
- [ ] add_client_id_to_open_id_connect_provider
- [X] add_role_to_instance_profile
@ -2922,7 +2922,7 @@
- [X] create_policy_version
- [X] create_role
- [X] create_saml_provider
- [ ] create_service_linked_role
- [X] create_service_linked_role
- [ ] create_service_specific_credential
- [X] create_user
- [X] create_virtual_mfa_device
@ -2942,7 +2942,7 @@
- [X] delete_role_policy
- [X] delete_saml_provider
- [X] delete_server_certificate
- [ ] delete_service_linked_role
- [X] delete_service_linked_role
- [ ] delete_service_specific_credential
- [X] delete_signing_certificate
- [X] delete_ssh_public_key
@ -2978,7 +2978,7 @@
- [X] get_server_certificate
- [ ] get_service_last_accessed_details
- [ ] get_service_last_accessed_details_with_entities
- [ ] get_service_linked_role_deletion_status
- [X] get_service_linked_role_deletion_status
- [X] get_ssh_public_key
- [X] get_user
- [X] get_user_policy

View File

@ -42,7 +42,7 @@ iam
- [X] create_policy_version
- [X] create_role
- [X] create_saml_provider
- [ ] create_service_linked_role
- [X] create_service_linked_role
- [ ] create_service_specific_credential
- [X] create_user
- [X] create_virtual_mfa_device
@ -64,7 +64,7 @@ iam
- [X] delete_role_policy
- [X] delete_saml_provider
- [X] delete_server_certificate
- [ ] delete_service_linked_role
- [X] delete_service_linked_role
- [ ] delete_service_specific_credential
- [X] delete_signing_certificate
- [X] delete_ssh_public_key
@ -106,7 +106,11 @@ iam
- [X] get_server_certificate
- [ ] get_service_last_accessed_details
- [ ] get_service_last_accessed_details_with_entities
- [ ] get_service_linked_role_deletion_status
- [X] get_service_linked_role_deletion_status
This method always succeeds for now - we do not yet keep track of deletions
- [X] get_ssh_public_key
- [X] get_user
- [X] get_user_policy

View File

@ -4,6 +4,7 @@ import os
import random
import string
import sys
import uuid
from datetime import datetime
import json
import re
@ -12,6 +13,7 @@ import time
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from jinja2 import Template
from urllib import parse
from moto.core.exceptions import RESTError
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel
@ -47,6 +49,15 @@ from .utils import (
from ..utilities.tagging_service import TaggingService
# Map to convert service names used in ServiceLinkedRoles
# The PascalCase should be used as part of the RoleName
SERVICE_NAME_CONVERSION = {
"autoscaling": "AutoScaling",
"application-autoscaling": "ApplicationAutoScaling",
"elasticbeanstalk": "ElasticBeanstalk",
}
class MFADevice(object):
"""MFA Device class."""
@ -556,6 +567,7 @@ class Role(CloudFormationModel):
description,
tags,
max_session_duration,
linked_service=None,
):
self.id = role_id
self.name = name
@ -568,6 +580,7 @@ class Role(CloudFormationModel):
self.description = description
self.permissions_boundary = permissions_boundary
self.max_session_duration = max_session_duration
self._linked_service = linked_service
@property
def created_iso_8601(self):
@ -622,6 +635,8 @@ class Role(CloudFormationModel):
@property
def arn(self):
if self._linked_service:
return f"arn:aws:iam::{ACCOUNT_ID}:role/aws-service-role/{self._linked_service}/{self.name}"
return "arn:aws:iam::{0}:role{1}{2}".format(ACCOUNT_ID, self.path, self.name)
def to_config_dict(self):
@ -722,6 +737,41 @@ class Role(CloudFormationModel):
return html.escape(self.description or "")
def to_xml(self):
template = Template(
"""<Role>
<Path>{{ role.path }}</Path>
<Arn>{{ role.arn }}</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
{% if role.description is not none %}
<Description>{{ role.description_escaped }}</Description>
{% endif %}
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
<RoleId>{{ role.id }}</RoleId>
{% if role.max_session_duration %}
<MaxSessionDuration>{{ role.max_session_duration }}</MaxSessionDuration>
{% endif %}
{% if role.permissions_boundary %}
<PermissionsBoundary>
<PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>
<PermissionsBoundaryArn>{{ role.permissions_boundary }}</PermissionsBoundaryArn>
</PermissionsBoundary>
{% endif %}
{% if role.tags %}
<Tags>
{% for tag in role.get_tags() %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
{% endif %}
</Role>"""
)
return template.render(role=self)
class InstanceProfile(CloudFormationModel):
def __init__(self, instance_profile_id, name, path, roles, tags=None):
@ -1707,6 +1757,7 @@ class IAMBackend(BaseBackend):
description,
tags,
max_session_duration,
linked_service=None,
):
role_id = random_resource_id()
if permissions_boundary and not self.policy_arn_regex.match(
@ -1733,6 +1784,7 @@ class IAMBackend(BaseBackend):
description,
clean_tags,
max_session_duration,
linked_service=linked_service,
)
self.roles[role_id] = role
return role
@ -2813,5 +2865,51 @@ class IAMBackend(BaseBackend):
self.tagger.untag_resource_using_names(user.arn, tag_keys)
def create_service_linked_role(self, service_name, description, suffix):
# service.amazonaws.com -> Service
# some-thing.service.amazonaws.com -> Service_SomeThing
service = service_name.split(".")[-3]
prefix = service_name.split(".")[0]
if service != prefix:
prefix = "".join([x.capitalize() for x in prefix.split("-")])
service = SERVICE_NAME_CONVERSION.get(service, service) + "_" + prefix
else:
service = SERVICE_NAME_CONVERSION.get(service, service)
role_name = f"AWSServiceRoleFor{service}"
if suffix:
role_name = role_name + f"_{suffix}"
assume_role_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Action": ["sts:AssumeRole"],
"Effect": "Allow",
"Principal": {"Service": [service_name]},
}
],
}
path = f"/aws-service-role/{service_name}/"
return self.create_role(
role_name,
json.dumps(assume_role_policy_document),
path,
permissions_boundary=None,
description=description,
tags=[],
max_session_duration=None,
linked_service=service_name,
)
def delete_service_linked_role(self, role_name):
self.delete_role(role_name)
deletion_task_id = str(uuid.uuid4())
return deletion_task_id
def get_service_linked_role_deletion_status(self):
"""
This method always succeeds for now - we do not yet keep track of deletions
"""
return True
iam_backend = IAMBackend()

View File

@ -1088,6 +1088,32 @@ class IamResponse(BaseResponse):
template = self.response_template(UNTAG_USER_TEMPLATE)
return template.render()
def create_service_linked_role(self):
service_name = self._get_param("AWSServiceName")
description = self._get_param("Description")
suffix = self._get_param("CustomSuffix")
role = iam_backend.create_service_linked_role(service_name, description, suffix)
template = self.response_template(CREATE_SERVICE_LINKED_ROLE_TEMPLATE)
return template.render(role=role)
def delete_service_linked_role(self):
role_name = self._get_param("RoleName")
deletion_task_id = iam_backend.delete_service_linked_role(role_name)
template = self.response_template(DELETE_SERVICE_LINKED_ROLE_TEMPLATE)
return template.render(deletion_task_id=deletion_task_id)
def get_service_linked_role_deletion_status(self):
iam_backend.get_service_linked_role_deletion_status()
template = self.response_template(
GET_SERVICE_LINKED_ROLE_DELETION_STATUS_TEMPLATE
)
return template.render()
LIST_ENTITIES_FOR_POLICY_TEMPLATE = """<ListEntitiesForPolicyResponse>
<ListEntitiesForPolicyResult>
@ -1399,34 +1425,7 @@ GET_INSTANCE_PROFILE_TEMPLATE = """<GetInstanceProfileResponse xmlns="https://ia
CREATE_ROLE_TEMPLATE = """<CreateRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<CreateRoleResult>
<Role>
<Path>{{ role.path }}</Path>
<Arn>{{ role.arn }}</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
{% if role.description is not none %}
<Description>{{ role.description_escaped }}</Description>
{% endif %}
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
<RoleId>{{ role.id }}</RoleId>
<MaxSessionDuration>{{ role.max_session_duration }}</MaxSessionDuration>
{% if role.permissions_boundary %}
<PermissionsBoundary>
<PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>
<PermissionsBoundaryArn>{{ role.permissions_boundary }}</PermissionsBoundaryArn>
</PermissionsBoundary>
{% endif %}
{% if role.tags %}
<Tags>
{% for tag in role.get_tags() %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
{% endif %}
</Role>
{{ role.to_xml() }}
</CreateRoleResult>
<ResponseMetadata>
<RequestId>4a93ceee-9966-11e1-b624-b1aEXAMPLE7c</RequestId>
@ -1444,6 +1443,33 @@ GET_ROLE_POLICY_TEMPLATE = """<GetRolePolicyResponse xmlns="https://iam.amazonaw
</ResponseMetadata>
</GetRolePolicyResponse>"""
CREATE_SERVICE_LINKED_ROLE_TEMPLATE = """<CreateServiceLinkedRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<CreateServiceLinkedRoleResult>
{{ role.to_xml() }}
</CreateServiceLinkedRoleResult>
<ResponseMetadata>
<RequestId>4a93ceee-9966-11e1-b624-b1aEXAMPLE7c</RequestId>
</ResponseMetadata>
</CreateServiceLinkedRoleResponse>"""
DELETE_SERVICE_LINKED_ROLE_TEMPLATE = """<DeleteServiceLinkedRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<DeleteServiceLinkedRoleResult>
<DeletionTaskId>{{ deletion_task_id }}</DeletionTaskId>
</DeleteServiceLinkedRoleResult>
<ResponseMetadata>
<RequestId>4a93ceee-9966-11e1-b624-b1aEXAMPLE7c</RequestId>
</ResponseMetadata>
</DeleteServiceLinkedRoleResponse>"""
GET_SERVICE_LINKED_ROLE_DELETION_STATUS_TEMPLATE = """<GetServiceLinkedRoleDeletionStatusResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetServiceLinkedRoleDeletionStatusResult>
<Status>SUCCEEDED</Status>
</GetServiceLinkedRoleDeletionStatusResult>
<ResponseMetadata>
<RequestId>4a93ceee-9966-11e1-b624-b1aEXAMPLE7c</RequestId>
</ResponseMetadata>
</GetServiceLinkedRoleDeletionStatusResponse>"""
UPDATE_ROLE_TEMPLATE = """<UpdateRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<UpdateRoleResult>
</UpdateRoleResult>
@ -1454,28 +1480,7 @@ UPDATE_ROLE_TEMPLATE = """<UpdateRoleResponse xmlns="https://iam.amazonaws.com/d
UPDATE_ROLE_DESCRIPTION_TEMPLATE = """<UpdateRoleDescriptionResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<UpdateRoleDescriptionResult>
<Role>
<Path>{{ role.path }}</Path>
<Arn>{{ role.arn }}</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
{% if role.description is not none %}
<Description>{{ role.description_escaped }}</Description>
{% endif %}
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
<RoleId>{{ role.id }}</RoleId>
<MaxSessionDuration>{{ role.max_session_duration }}</MaxSessionDuration>
{% if role.tags %}
<Tags>
{% for tag in role.get_tags() %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
{% endif %}
</Role>
{{ role.to_xml() }}
</UpdateRoleDescriptionResult>
<ResponseMetadata>
<RequestId>df37e965-9967-11e1-a4c3-270EXAMPLE04</RequestId>
@ -1484,34 +1489,7 @@ UPDATE_ROLE_DESCRIPTION_TEMPLATE = """<UpdateRoleDescriptionResponse xmlns="http
GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetRoleResult>
<Role>
<Path>{{ role.path }}</Path>
<Arn>{{ role.arn }}</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
{% if role.description is not none %}
<Description>{{ role.description_escaped }}</Description>
{% endif %}
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
<RoleId>{{ role.id }}</RoleId>
<MaxSessionDuration>{{ role.max_session_duration }}</MaxSessionDuration>
{% if role.permissions_boundary %}
<PermissionsBoundary>
<PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>
<PermissionsBoundaryArn>{{ role.permissions_boundary }}</PermissionsBoundaryArn>
</PermissionsBoundary>
{% endif %}
{% if role.tags %}
<Tags>
{% for tag in role.get_tags() %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
{% endif %}
</Role>
{{ role.to_xml() }}
</GetRoleResult>
<ResponseMetadata>
<RequestId>df37e965-9967-11e1-a4c3-270EXAMPLE04</RequestId>

View File

@ -25,7 +25,7 @@ index 51e5d1c9c7..057446ae1d 100644
Target: []string{iam.DeletionTaskStatusTypeSucceeded},
Refresh: statusDeleteServiceLinkedRole(conn, deletionTaskID),
- Timeout: 5 * time.Minute,
+ Timeout: 5 * time.Second,
+ Timeout: 15 * time.Second,
Delay: 10 * time.Second,
}

View File

@ -91,6 +91,7 @@ iam:
- TestAccIAMRolePolicy_
- TestAccIAMRolePolicyAttachment_
- TestAccIAMSessionContextDataSource_
- TestAccIAMServiceLinkedRole
- TestAccIAMUserDataSource_
- TestAccIAMUserPolicy_
- TestAccIAMUserPolicyAttachment_
@ -100,6 +101,10 @@ iot:
- TestAccIoTEndpointDataSource
kms:
- TestAccKMSAlias
- TestAccKMSKey_Policy_basic
- TestAccKMSKey_Policy_iamRole
- TestAccKMSKey_Policy_iamRoleOrder
- TestAccKMSKey_Policy_iamServiceLinkedRole
- TestAccKMSSecretDataSource
- TestAccKMSSecretsDataSource
meta:

View File

@ -4407,3 +4407,82 @@ def test_untag_user_error_unknown_user_name():
ex.response["Error"]["Message"].should.equal(
"The user with name {} cannot be found.".format(name)
)
@mock_iam
@pytest.mark.parametrize(
"service,cased",
[
("autoscaling", "AutoScaling"),
("elasticbeanstalk", "ElasticBeanstalk"),
(
"custom-resource.application-autoscaling",
"ApplicationAutoScaling_CustomResource",
),
("other", "other"),
],
)
def test_create_service_linked_role(service, cased):
client = boto3.client("iam", region_name="eu-central-1")
resp = client.create_service_linked_role(
AWSServiceName=f"{service}.amazonaws.com", Description="desc"
)["Role"]
resp.should.have.key("RoleName").equals(f"AWSServiceRoleFor{cased}")
@mock_iam
def test_create_service_linked_role__with_suffix():
client = boto3.client("iam", region_name="eu-central-1")
resp = client.create_service_linked_role(
AWSServiceName="autoscaling.amazonaws.com",
CustomSuffix="suf",
Description="desc",
)["Role"]
resp.should.have.key("RoleName").match("_suf$")
resp.should.have.key("Description").equals("desc")
resp.should.have.key("AssumeRolePolicyDocument")
policy_doc = resp["AssumeRolePolicyDocument"]
policy_doc.should.have.key("Statement").equals(
[
{
"Action": ["sts:AssumeRole"],
"Effect": "Allow",
"Principal": {"Service": ["autoscaling.amazonaws.com"]},
}
]
)
@mock_iam
def test_delete_service_linked_role():
client = boto3.client("iam", region_name="eu-central-1")
role_name = client.create_service_linked_role(
AWSServiceName="autoscaling.amazonaws.com",
CustomSuffix="suf",
Description="desc",
)["Role"]["RoleName"]
# Role exists
client.get_role(RoleName=role_name)
# Delete role
resp = client.delete_service_linked_role(RoleName=role_name)
resp.should.have.key("DeletionTaskId")
# Role deletion should be successful
resp = client.get_service_linked_role_deletion_status(
DeletionTaskId=resp["DeletionTaskId"]
)
resp.should.have.key("Status").equals("SUCCEEDED")
# Role no longer exists
with pytest.raises(ClientError) as ex:
client.get_role(RoleName=role_name)
err = ex.value.response["Error"]
err["Code"].should.equal("NoSuchEntity")
err["Message"].should.contain("not found")