IAM - Implement ServiceLinkedRoles (#5089)
This commit is contained in:
parent
578de3d47f
commit
76fe578d95
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user