From 1d9382b5e5b177beacd4a143eed5bb37a0d4f15d Mon Sep 17 00:00:00 2001 From: gruebel Date: Mon, 21 Oct 2019 21:48:50 +0200 Subject: [PATCH] Add iam.list_virtual_mfa_devices --- IMPLEMENTATION_COVERAGE.md | 4 +- moto/iam/models.py | 26 +++++++++++ moto/iam/responses.py | 44 ++++++++++++++++++ tests/test_iam/test_iam.py | 93 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 2 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index e1ed8b2cb..d965e489a 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3163,7 +3163,7 @@ - [ ] describe_events ## iam -60% implemented +61% implemented - [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group @@ -3268,7 +3268,7 @@ - [X] list_user_policies - [ ] list_user_tags - [X] list_users -- [ ] list_virtual_mfa_devices +- [X] list_virtual_mfa_devices - [X] put_group_policy - [ ] put_role_permissions_boundary - [X] put_role_policy diff --git a/moto/iam/models.py b/moto/iam/models.py index 5ac1ea3b5..8921244cd 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -53,6 +53,7 @@ class VirtualMfaDevice(object): self.enable_date = None self.user_attribute = None + self.user = None @property def enabled_iso_8601(self): @@ -1301,6 +1302,31 @@ class IAMBackend(BaseBackend): if not device: raise IAMNotFoundException('VirtualMFADevice with serial number {0} doesn\'t exist.'.format(serial_number)) + def list_virtual_mfa_devices(self, assignment_status, marker, max_items): + devices = list(self.virtual_mfa_devices.values()) + + if assignment_status == 'Assigned': + devices = [device for device in devices if device.enable_date] + + if assignment_status == 'Unassigned': + devices = [device for device in devices if not device.enable_date] + + sorted(devices, key=lambda device: device.serial_number) + max_items = int(max_items) + start_idx = int(marker) if marker else 0 + + if start_idx > len(devices): + raise ValidationError('Invalid Marker.') + + devices = devices[start_idx:start_idx + max_items] + + if len(devices) < max_items: + marker = None + else: + marker = str(start_idx + max_items) + + return devices, marker + def delete_user(self, user_name): try: del self.users[user_name] diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 8afe85e49..8814a25a1 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -615,6 +615,16 @@ class IamResponse(BaseResponse): template = self.response_template(DELETE_VIRTUAL_MFA_DEVICE_TEMPLATE) return template.render() + def list_virtual_mfa_devices(self): + assignment_status = self._get_param('AssignmentStatus', 'Any') + marker = self._get_param('Marker') + max_items = self._get_param('MaxItems', 100) + + devices, marker = iam_backend.list_virtual_mfa_devices(assignment_status, marker, max_items) + + template = self.response_template(LIST_VIRTUAL_MFA_DEVICES_TEMPLATE) + return template.render(devices=devices, marker=marker) + def delete_user(self): user_name = self._get_param('UserName') iam_backend.delete_user(user_name) @@ -1712,6 +1722,40 @@ DELETE_VIRTUAL_MFA_DEVICE_TEMPLATE = """ + + {% if marker is none %} + false + {% else %} + true + {{ marker }} + {% endif %} + + {% for device in devices %} + + {{ device.serial_number }} + {% if device.enable_date %} + {{ device.enabled_iso_8601 }} + {% endif %} + {% if device.user %} + + {{ user.path }} + {{ user.name }} + {{ user.id }} + {{ user.created_iso_8601 }} + {{ user.arn }} + + {% endif %} + + {% endfor %} + + + + b61ce1b1-0401-11e1-b2f8-2dEXAMPLEbfc + +""" + + LIST_ACCOUNT_ALIASES_TEMPLATE = """ false diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index d765e4b87..ff02646c4 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -812,6 +812,11 @@ def test_delete_virtual_mfa_device(): SerialNumber=serial_number ) + response = client.list_virtual_mfa_devices() + + response['VirtualMFADevices'].should.have.length_of(0) + response['IsTruncated'].should_not.be.ok + @mock_iam def test_delete_virtual_mfa_device_errors(): @@ -826,6 +831,94 @@ def test_delete_virtual_mfa_device_errors(): ) +@mock_iam +def test_list_virtual_mfa_devices(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_virtual_mfa_device( + VirtualMFADeviceName='test-device' + ) + serial_number_1 = response['VirtualMFADevice']['SerialNumber'] + + response = client.create_virtual_mfa_device( + Path='/test/', + VirtualMFADeviceName='test-device' + ) + serial_number_2 = response['VirtualMFADevice']['SerialNumber'] + + response = client.list_virtual_mfa_devices() + + response['VirtualMFADevices'].should.equal([ + { + 'SerialNumber': serial_number_1 + }, + { + 'SerialNumber': serial_number_2 + } + ]) + response['IsTruncated'].should_not.be.ok + + response = client.list_virtual_mfa_devices( + AssignmentStatus='Assigned' + ) + + response['VirtualMFADevices'].should.have.length_of(0) + response['IsTruncated'].should_not.be.ok + + response = client.list_virtual_mfa_devices( + AssignmentStatus='Unassigned' + ) + + response['VirtualMFADevices'].should.equal([ + { + 'SerialNumber': serial_number_1 + }, + { + 'SerialNumber': serial_number_2 + } + ]) + response['IsTruncated'].should_not.be.ok + + response = client.list_virtual_mfa_devices( + AssignmentStatus='Any', + MaxItems=1 + ) + + response['VirtualMFADevices'].should.equal([ + { + 'SerialNumber': serial_number_1 + } + ]) + response['IsTruncated'].should.be.ok + response['Marker'].should.equal('1') + + response = client.list_virtual_mfa_devices( + AssignmentStatus='Any', + Marker=response['Marker'] + ) + + response['VirtualMFADevices'].should.equal([ + { + 'SerialNumber': serial_number_2 + } + ]) + response['IsTruncated'].should_not.be.ok + + +@mock_iam +def test_list_virtual_mfa_devices_errors(): + client = boto3.client('iam', region_name='us-east-1') + client.create_virtual_mfa_device( + VirtualMFADeviceName='test-device' + ) + + client.list_virtual_mfa_devices.when.called_with( + Marker='100' + ).should.throw( + ClientError, + 'Invalid Marker.' + ) + + @mock_iam_deprecated() def test_delete_user_deprecated(): conn = boto.connect_iam()