diff --git a/moto/iam/models.py b/moto/iam/models.py index ac8402e5..f16cf10a 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -45,6 +45,7 @@ from .utils import ( random_resource_id, random_policy_id, ) +from ..utilities.tagging_service import TaggingService class MFADevice(object): @@ -924,7 +925,7 @@ class Group(BaseModel): class User(CloudFormationModel): - def __init__(self, name, path=None, tags=None): + def __init__(self, name, path=None): self.name = name self.id = random_resource_id() self.path = path if path else "/" @@ -937,7 +938,6 @@ class User(CloudFormationModel): self.password = None self.password_reset_required = False self.signing_certificates = {} - self.tags = tags @property def arn(self): @@ -1135,7 +1135,8 @@ class User(CloudFormationModel): ): properties = cloudformation_json.get("Properties", {}) path = properties.get("Path") - return iam_backend.create_user(resource_physical_name, path) + user, _ = iam_backend.create_user(resource_physical_name, path) + return user @classmethod def update_from_cloudformation_json( @@ -1415,6 +1416,8 @@ class IAMBackend(BaseBackend): self.account_summary = AccountSummary(self) self.inline_policies = {} self.access_keys = {} + + self.tagger = TaggingService() super(IAMBackend, self).__init__() def _init_managed_policies(self): @@ -1978,16 +1981,16 @@ class IAMBackend(BaseBackend): "EntityAlreadyExists", "User {0} already exists".format(user_name) ) - user = User(user_name, path, tags) + user = User(user_name, path) + self.tagger.tag_resource(user.arn, tags or []) self.users[user_name] = user - return user + return user, self.tagger.list_tags_for_resource(user.arn) - def get_user(self, user_name): - user = None - try: - user = self.users[user_name] - except KeyError: - raise IAMNotFoundException("User {0} not found".format(user_name)) + def get_user(self, name): + user = self.users.get(name) + + if not user: + raise NoSuchEntity("The user with name {} cannot be found.".format(name)) return user @@ -2147,7 +2150,7 @@ class IAMBackend(BaseBackend): def list_user_tags(self, user_name): user = self.get_user(user_name) - return user.tags + return self.tagger.list_tags_for_resource(user.arn) def put_user_policy(self, user_name, policy_name, policy_json): user = self.get_user(user_name) @@ -2204,7 +2207,7 @@ class IAMBackend(BaseBackend): try: # User may have been deleted before their access key... user = self.get_user(key.user_name) user.delete_access_key(key.access_key_id) - except IAMNotFoundException: + except NoSuchEntity: pass del self.access_keys[name] @@ -2250,7 +2253,7 @@ class IAMBackend(BaseBackend): "CreateDate": user.created_iso_8601, "PasswordLastUsed": None, # not supported "PermissionsBoundary": {}, # ToDo: add put_user_permissions_boundary() functionality - "Tags": {}, # ToDo: add tag_user() functionality + "Tags": self.tagger.list_tags_for_resource(user.arn)["Tags"], } user.enable_mfa_device( @@ -2355,6 +2358,7 @@ class IAMBackend(BaseBackend): code="DeleteConflict", message="Cannot delete entity, must delete policies first.", ) + self.tagger.delete_all_tags_for_resource(user.arn) del self.users[user_name] def report_generated(self): @@ -2574,5 +2578,15 @@ class IAMBackend(BaseBackend): inline_policy.unapply_policy(self) del self.inline_policies[policy_id] + def tag_user(self, name, tags): + user = self.get_user(name) + + self.tagger.tag_resource(user.arn, tags) + + def untag_user(self, name, tag_keys): + user = self.get_user(name) + + self.tagger.untag_resource_using_names(user.arn, tag_keys) + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index d6f8ae02..03ce13d2 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -471,9 +471,9 @@ class IamResponse(BaseResponse): user_name = self._get_param("UserName") path = self._get_param("Path") tags = self._get_multi_param("Tags.member") - user = iam_backend.create_user(user_name, path, tags) + user, user_tags = iam_backend.create_user(user_name, path, tags) template = self.response_template(USER_TEMPLATE) - return template.render(action="Create", user=user) + return template.render(action="Create", user=user, tags=user_tags["Tags"]) def get_user(self): user_name = self._get_param("UserName") @@ -572,7 +572,7 @@ class IamResponse(BaseResponse): user_name = self._get_param("UserName") tags = iam_backend.list_user_tags(user_name) template = self.response_template(LIST_USER_TAGS_TEMPLATE) - return template.render(user_tags=tags or []) + return template.render(user_tags=tags["Tags"]) def put_user_policy(self): user_name = self._get_param("UserName") @@ -989,6 +989,24 @@ class IamResponse(BaseResponse): template = self.response_template(GET_ACCOUNT_SUMMARY_TEMPLATE) return template.render(summary_map=account_summary.summary_map) + def tag_user(self): + name = self._get_param("UserName") + tags = self._get_multi_param("Tags.member") + + iam_backend.tag_user(name, tags) + + template = self.response_template(TAG_USER_TEMPLATE) + return template.render() + + def untag_user(self): + name = self._get_param("UserName") + tag_keys = self._get_multi_param("TagKeys.member") + + iam_backend.untag_user(name, tag_keys) + + template = self.response_template(UNTAG_USER_TEMPLATE) + return template.render() + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -1684,9 +1702,9 @@ USER_TEMPLATE = """<{{ action }}UserResponse> {{ user.id }} {{ user.created_iso_8601 }} {{ user.arn }} - {% if user.tags %} + {% if tags %} - {% for tag in user.tags %} + {% for tag in tags %} {{ tag['Key'] }} {{ tag['Value'] }} @@ -2039,13 +2057,23 @@ LIST_VIRTUAL_MFA_DEVICES_TEMPLATE = """ + + EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE + +""" + + +UNTAG_USER_TEMPLATE = """ + + EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index ae30b77f..ab4eb23a 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -1140,8 +1140,9 @@ def test_enable_virtual_mfa_device(): client = boto3.client("iam", region_name="us-east-1") response = client.create_virtual_mfa_device(VirtualMFADeviceName="test-device") serial_number = response["VirtualMFADevice"]["SerialNumber"] + tags = [{"Key": "key", "Value": "value"}] - client.create_user(UserName="test-user") + client.create_user(UserName="test-user", Tags=tags) client.enable_mfa_device( UserName="test-user", SerialNumber=serial_number, @@ -1165,6 +1166,7 @@ def test_enable_virtual_mfa_device(): "arn:aws:iam::{}:user/test-user".format(ACCOUNT_ID) ) device["User"]["CreateDate"].should.be.a(datetime) + device["User"]["Tags"].should.equal(tags) device["EnableDate"].should.be.a(datetime) response["IsTruncated"].should_not.be.ok @@ -2924,7 +2926,7 @@ def test_list_user_tags(): ], ) response = conn.list_user_tags(UserName="kenny-bania") - response["Tags"].should.equal([]) + response["Tags"].should.have.length_of(0) response["IsTruncated"].should_not.be.ok response = conn.list_user_tags(UserName="jackie-chiles") @@ -4047,3 +4049,80 @@ def test_create_user_with_tags(): resp = conn.create_user(UserName="test-create-user-no-tags") assert "Tags" not in resp["User"] + + +@mock_iam +def test_tag_user(): + # given + client = boto3.client("iam", region_name="eu-central-1") + name = "test-user" + tags = sorted( + [{"Key": "key", "Value": "value"}, {"Key": "key-2", "Value": "value-2"}], + key=lambda item: item["Key"], + ) + client.create_user(UserName=name) + + # when + client.tag_user(UserName=name, Tags=tags) + + # then + response = client.list_user_tags(UserName=name) + sorted(response["Tags"], key=lambda item: item["Key"],).should.equal(tags) + + +@mock_iam +def test_tag_user_error_unknown_user_name(): + # given + client = boto3.client("iam", region_name="eu-central-1") + name = "unknown" + + # when + with pytest.raises(ClientError) as e: + client.tag_user(UserName=name, Tags=[{"Key": "key", "Value": "value"}]) + + # then + ex = e.value + ex.operation_name.should.equal("TagUser") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404) + ex.response["Error"]["Code"].should.contain("NoSuchEntity") + ex.response["Error"]["Message"].should.equal( + "The user with name {} cannot be found.".format(name) + ) + + +@mock_iam +def test_untag_user(): + # given + client = boto3.client("iam", region_name="eu-central-1") + name = "test-user" + client.create_user( + UserName=name, + Tags=[{"Key": "key", "Value": "value"}, {"Key": "key-2", "Value": "value"}], + ) + + # when + client.untag_user(UserName=name, TagKeys=["key-2"]) + + # then + response = client.list_user_tags(UserName=name) + response["Tags"].should.equal([{"Key": "key", "Value": "value"}]) + + +@mock_iam +def test_untag_user_error_unknown_user_name(): + # given + client = boto3.client("iam", region_name="eu-central-1") + name = "unknown" + + # when + with pytest.raises(ClientError) as e: + client.untag_user(UserName=name, TagKeys=["key"]) + + # then + ex = e.value + ex.operation_name.should.equal("UntagUser") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404) + ex.response["Error"]["Code"].should.contain("NoSuchEntity") + ex.response["Error"]["Message"].should.equal( + "The user with name {} cannot be found.".format(name) + ) diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py index a50ed823..f4b2e51b 100644 --- a/tests/test_iam/test_iam_cloudformation.py +++ b/tests/test_iam/test_iam_cloudformation.py @@ -967,59 +967,6 @@ Outputs: pass -@mock_iam -@mock_cloudformation -def test_iam_cloudformation_delete_users_access_key(): - cf_client = boto3.client("cloudformation", region_name="us-east-1") - - stack_name = "MyStack" - - template = """ - Resources: - TheUser: - Type: AWS::IAM::User - TheAccessKey: - Type: AWS::IAM::AccessKey - Properties: - UserName: !Ref TheUser - """.strip() - - cf_client.create_stack(StackName=stack_name, TemplateBody=template) - - provisioned_resources = cf_client.list_stack_resources(StackName=stack_name)[ - "StackResourceSummaries" - ] - - provisioned_user = [ - resource - for resource in provisioned_resources - if resource["LogicalResourceId"] == "TheUser" - ][0] - user_name = provisioned_user["PhysicalResourceId"] - - provisioned_access_key = [ - resource - for resource in provisioned_resources - if resource["LogicalResourceId"] == "TheAccessKey" - ][0] - access_key_id = provisioned_access_key["PhysicalResourceId"] - - iam_client = boto3.client("iam", region_name="us-east-1") - user = iam_client.get_user(UserName=user_name) - access_keys = iam_client.list_access_keys(UserName=user_name) - - access_key_id.should.equal(access_keys["AccessKeyMetadata"][0]["AccessKeyId"]) - - cf_client.delete_stack(StackName=stack_name) - - iam_client.get_user.when.called_with(UserName=user_name).should.throw( - iam_client.exceptions.NoSuchEntityException - ) - iam_client.list_access_keys.when.called_with(UserName=user_name).should.throw( - iam_client.exceptions.NoSuchEntityException - ) - - @mock_iam @mock_cloudformation def test_iam_cloudformation_delete_users_access_key(): @@ -1055,13 +1002,15 @@ def test_iam_cloudformation_delete_users_access_key(): for resource in provisioned_resources if resource["LogicalResourceId"] == "TheAccessKey" ] - len(provisioned_access_keys).should.equal(1) + provisioned_access_keys.should.have.length_of(1) + access_key_id = provisioned_access_keys[0]["PhysicalResourceId"] iam_client = boto3.client("iam", region_name="us-east-1") user = iam_client.get_user(UserName=user_name)["User"] user["UserName"].should.equal(user_name) access_keys = iam_client.list_access_keys(UserName=user_name) access_keys["AccessKeyMetadata"][0]["UserName"].should.equal(user_name) + access_key_id.should.equal(access_keys["AccessKeyMetadata"][0]["AccessKeyId"]) cf_client.delete_stack(StackName=stack_name)