Merge pull request #2041 from mikegrima/roletags
IAM Role Tagging support
This commit is contained in:
commit
f41c4e756f
6 changed files with 496 additions and 47 deletions
|
|
@ -32,3 +32,48 @@ class MalformedCertificate(RESTError):
|
|||
def __init__(self, cert):
|
||||
super(MalformedCertificate, self).__init__(
|
||||
'MalformedCertificate', 'Certificate {cert} is malformed'.format(cert=cert))
|
||||
|
||||
|
||||
class DuplicateTags(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(DuplicateTags, self).__init__(
|
||||
'InvalidInput', 'Duplicate tag keys found. Please note that Tag keys are case insensitive.')
|
||||
|
||||
|
||||
class TagKeyTooBig(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tag, param='tags.X.member.key'):
|
||||
super(TagKeyTooBig, self).__init__(
|
||||
'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
|
||||
"constraint: Member must have length less than or equal to 128.".format(tag, param))
|
||||
|
||||
|
||||
class TagValueTooBig(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tag):
|
||||
super(TagValueTooBig, self).__init__(
|
||||
'ValidationError', "1 validation error detected: Value '{}' at 'tags.X.member.value' failed to satisfy "
|
||||
"constraint: Member must have length less than or equal to 256.".format(tag))
|
||||
|
||||
|
||||
class InvalidTagCharacters(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tag, param='tags.X.member.key'):
|
||||
message = "1 validation error detected: Value '{}' at '{}' failed to satisfy ".format(tag, param)
|
||||
message += "constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+"
|
||||
|
||||
super(InvalidTagCharacters, self).__init__('ValidationError', message)
|
||||
|
||||
|
||||
class TooManyTags(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, tags, param='tags'):
|
||||
super(TooManyTags, self).__init__(
|
||||
'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
|
||||
"constraint: Member must have length less than or equal to 50.".format(tags, param))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import base64
|
|||
import sys
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
|
@ -12,7 +13,8 @@ from moto.core import BaseBackend, BaseModel
|
|||
from moto.core.utils import iso_8601_datetime_without_milliseconds
|
||||
|
||||
from .aws_managed_policies import aws_managed_policies_data
|
||||
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate
|
||||
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate, \
|
||||
DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig
|
||||
from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
|
||||
|
||||
ACCOUNT_ID = 123456789012
|
||||
|
|
@ -32,7 +34,6 @@ class MFADevice(object):
|
|||
|
||||
|
||||
class Policy(BaseModel):
|
||||
|
||||
is_attachable = False
|
||||
|
||||
def __init__(self,
|
||||
|
|
@ -132,6 +133,7 @@ class Role(BaseModel):
|
|||
self.policies = {}
|
||||
self.managed_policies = {}
|
||||
self.create_date = datetime.now(pytz.utc)
|
||||
self.tags = {}
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
|
|
@ -175,6 +177,9 @@ class Role(BaseModel):
|
|||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
def get_tags(self):
|
||||
return [self.tags[tag] for tag in self.tags]
|
||||
|
||||
|
||||
class InstanceProfile(BaseModel):
|
||||
|
||||
|
|
@ -614,6 +619,86 @@ class IAMBackend(BaseBackend):
|
|||
role = self.get_role(role_name)
|
||||
return role.policies.keys()
|
||||
|
||||
def _validate_tag_key(self, tag_key, exception_param='tags.X.member.key'):
|
||||
"""Validates the tag key.
|
||||
|
||||
:param all_tags: Dict to check if there is a duplicate tag.
|
||||
:param tag_key: The tag key to check against.
|
||||
:param exception_param: The exception parameter to send over to help format the message. This is to reflect
|
||||
the difference between the tag and untag APIs.
|
||||
:return:
|
||||
"""
|
||||
# Validate that the key length is correct:
|
||||
if len(tag_key) > 128:
|
||||
raise TagKeyTooBig(tag_key, param=exception_param)
|
||||
|
||||
# Validate that the tag key fits the proper Regex:
|
||||
# [\w\s_.:/=+\-@]+ SHOULD be the same as the Java regex on the AWS documentation: [\p{L}\p{Z}\p{N}_.:/=+\-@]+
|
||||
match = re.findall(r'[\w\s_.:/=+\-@]+', tag_key)
|
||||
# Kudos if you can come up with a better way of doing a global search :)
|
||||
if not len(match) or len(match[0]) < len(tag_key):
|
||||
raise InvalidTagCharacters(tag_key, param=exception_param)
|
||||
|
||||
def _check_tag_duplicate(self, all_tags, tag_key):
|
||||
"""Validates that a tag key is not a duplicate
|
||||
|
||||
:param all_tags: Dict to check if there is a duplicate tag.
|
||||
:param tag_key: The tag key to check against.
|
||||
:return:
|
||||
"""
|
||||
if tag_key in all_tags:
|
||||
raise DuplicateTags()
|
||||
|
||||
def list_role_tags(self, role_name, marker, max_items=100):
|
||||
role = self.get_role(role_name)
|
||||
|
||||
max_items = int(max_items)
|
||||
tag_index = sorted(role.tags)
|
||||
start_idx = int(marker) if marker else 0
|
||||
|
||||
tag_index = tag_index[start_idx:start_idx + max_items]
|
||||
|
||||
if len(role.tags) <= (start_idx + max_items):
|
||||
marker = None
|
||||
else:
|
||||
marker = str(start_idx + max_items)
|
||||
|
||||
# Make the tag list of dict's:
|
||||
tags = [role.tags[tag] for tag in tag_index]
|
||||
|
||||
return tags, marker
|
||||
|
||||
def tag_role(self, role_name, tags):
|
||||
if len(tags) > 50:
|
||||
raise TooManyTags(tags)
|
||||
|
||||
role = self.get_role(role_name)
|
||||
|
||||
tag_keys = {}
|
||||
for tag in tags:
|
||||
# Need to index by the lowercase tag key since the keys are case insensitive, but their case is retained.
|
||||
ref_key = tag['Key'].lower()
|
||||
self._check_tag_duplicate(tag_keys, ref_key)
|
||||
self._validate_tag_key(tag['Key'])
|
||||
if len(tag['Value']) > 256:
|
||||
raise TagValueTooBig(tag['Value'])
|
||||
|
||||
tag_keys[ref_key] = tag
|
||||
|
||||
role.tags.update(tag_keys)
|
||||
|
||||
def untag_role(self, role_name, tag_keys):
|
||||
if len(tag_keys) > 50:
|
||||
raise TooManyTags(tag_keys, param='tagKeys')
|
||||
|
||||
role = self.get_role(role_name)
|
||||
|
||||
for key in tag_keys:
|
||||
ref_key = key.lower()
|
||||
self._validate_tag_key(key, exception_param='tagKeys')
|
||||
|
||||
role.tags.pop(ref_key, None)
|
||||
|
||||
def create_policy_version(self, policy_arn, policy_document, set_as_default):
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
|
|
|
|||
|
|
@ -554,7 +554,8 @@ class IamResponse(BaseResponse):
|
|||
policies=account_details['managed_policies'],
|
||||
users=account_details['users'],
|
||||
groups=account_details['groups'],
|
||||
roles=account_details['roles']
|
||||
roles=account_details['roles'],
|
||||
get_groups_for_user=iam_backend.get_groups_for_user
|
||||
)
|
||||
|
||||
def create_saml_provider(self):
|
||||
|
|
@ -625,6 +626,34 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(LIST_SIGNING_CERTIFICATES_TEMPLATE)
|
||||
return template.render(user_name=user_name, certificates=certs)
|
||||
|
||||
def list_role_tags(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
marker = self._get_param('Marker')
|
||||
max_items = self._get_param('MaxItems', 100)
|
||||
|
||||
tags, marker = iam_backend.list_role_tags(role_name, marker, max_items)
|
||||
|
||||
template = self.response_template(LIST_ROLE_TAG_TEMPLATE)
|
||||
return template.render(tags=tags, marker=marker)
|
||||
|
||||
def tag_role(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
tags = self._get_multi_param('Tags.member')
|
||||
|
||||
iam_backend.tag_role(role_name, tags)
|
||||
|
||||
template = self.response_template(TAG_ROLE_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
def untag_role(self):
|
||||
role_name = self._get_param('RoleName')
|
||||
tag_keys = self._get_multi_param('TagKeys.member')
|
||||
|
||||
iam_backend.untag_role(role_name, tag_keys)
|
||||
|
||||
template = self.response_template(UNTAG_ROLE_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
|
||||
ATTACH_ROLE_POLICY_TEMPLATE = """<AttachRolePolicyResponse>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -878,6 +907,16 @@ GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/201
|
|||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.tags %}
|
||||
<Tags>
|
||||
{% for tag in role.get_tags() %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
{% endif %}
|
||||
</Role>
|
||||
</GetRoleResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1461,8 +1500,19 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<UserDetailList>
|
||||
{% for user in users %}
|
||||
<member>
|
||||
<GroupList />
|
||||
<AttachedManagedPolicies/>
|
||||
<GroupList>
|
||||
{% for group in get_groups_for_user(user.name) %}
|
||||
<member>{{ group.name }}</member>
|
||||
{% endfor %}
|
||||
</GroupList>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in user.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ user.managed_policies[policy].name }}</PolicyName>
|
||||
<PolicyArn>{{ policy }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<UserId>{{ user.id }}</UserId>
|
||||
<Path>{{ user.path }}</Path>
|
||||
<UserName>{{ user.name }}</UserName>
|
||||
|
|
@ -1476,25 +1526,39 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<member>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in group.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
</member>
|
||||
{% for policy_arn in group.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ group.managed_policies[policy_arn].name }}</PolicyName>
|
||||
<PolicyArn>{{ policy_arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<GroupName>{{ group.name }}</GroupName>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.create_date }}</CreateDate>
|
||||
<GroupPolicyList/>
|
||||
<GroupPolicyList>
|
||||
{% for policy in group.policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy }}</PolicyName>
|
||||
<PolicyDocument>{{ group.get_policy(policy) }}</PolicyDocument>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</GroupPolicyList>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</GroupDetailList>
|
||||
<RoleDetailList>
|
||||
{% for role in roles %}
|
||||
<member>
|
||||
<RolePolicyList/>
|
||||
<RolePolicyList>
|
||||
{% for inline_policy in role.policies %}
|
||||
<member>
|
||||
<PolicyName>{{ inline_policy }}</PolicyName>
|
||||
<PolicyDocument>{{ role.policies[inline_policy] }}</PolicyDocument>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</RolePolicyList>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in role.managed_policies %}
|
||||
<member>
|
||||
|
|
@ -1503,6 +1567,14 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<Tags>
|
||||
{% for tag in role.get_tags() %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
<InstanceProfileList>
|
||||
{% for profile in instance_profiles %}
|
||||
<member>
|
||||
|
|
@ -1543,19 +1615,14 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<PolicyVersionList>
|
||||
{% for policy_version in policy.versions %}
|
||||
<member>
|
||||
<Document>
|
||||
{"Version":"2012-10-17","Statement":{"Effect":"Allow",
|
||||
"Action":["iam:CreatePolicy","iam:CreatePolicyVersion",
|
||||
"iam:DeletePolicy","iam:DeletePolicyVersion","iam:GetPolicy",
|
||||
"iam:GetPolicyVersion","iam:ListPolicies",
|
||||
"iam:ListPolicyVersions","iam:SetDefaultPolicyVersion"],
|
||||
"Resource":"*"}}
|
||||
</Document>
|
||||
<IsDefaultVersion>true</IsDefaultVersion>
|
||||
<VersionId>v1</VersionId>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
<Document>{{ policy_version.document }}</Document>
|
||||
<IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
|
||||
<VersionId>{{ policy_version.version_id }}</VersionId>
|
||||
<CreateDate>{{ policy_version.create_datetime }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</PolicyVersionList>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>1</AttachmentCount>
|
||||
|
|
@ -1671,3 +1738,38 @@ LIST_SIGNING_CERTIFICATES_TEMPLATE = """<ListSigningCertificatesResponse>
|
|||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListSigningCertificatesResponse>"""
|
||||
|
||||
|
||||
TAG_ROLE_TEMPLATE = """<TagRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</TagRoleResponse>"""
|
||||
|
||||
|
||||
LIST_ROLE_TAG_TEMPLATE = """<ListRoleTagsResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ListRoleTagsResult>
|
||||
<IsTruncated>{{ 'true' if marker else 'false' }}</IsTruncated>
|
||||
{% if marker %}
|
||||
<Marker>{{ marker }}</Marker>
|
||||
{% endif %}
|
||||
<Tags>
|
||||
{% for tag in tags %}
|
||||
<member>
|
||||
<Key>{{ tag['Key'] }}</Key>
|
||||
<Value>{{ tag['Value'] }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
</ListRoleTagsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListRoleTagsResponse>"""
|
||||
|
||||
|
||||
UNTAG_ROLE_TEMPLATE = """<UntagRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</UntagRoleResponse>"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue