diff --git a/moto/iam/models.py b/moto/iam/models.py index c150d1c99..c219c1afc 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -11,6 +11,19 @@ from .utils import random_access_key, random_alphanumeric, random_resource_id, r ACCOUNT_ID = 123456789012 +class MFADevice(object): + """MFA Device class.""" + + def __init__(self, + serial_number, + authentication_code_1, + authentication_code_2): + self.enable_date = datetime.now(pytz.utc) + self.serial_number = serial_number + self.authentication_code_1 = authentication_code_1 + self.authentication_code_2 = authentication_code_2 + + class Policy(BaseModel): is_attachable = False @@ -226,6 +239,7 @@ class User(BaseModel): datetime.utcnow(), "%Y-%m-%d-%H-%M-%S" ) + self.mfa_devices = {} self.policies = {} self.access_keys = [] self.password = None @@ -251,6 +265,9 @@ class User(BaseModel): def put_policy(self, policy_name, policy_json): self.policies[policy_name] = policy_json + def deactivate_mfa_device(self, serial_number): + self.mfa_devices.pop(serial_number) + def delete_policy(self, policy_name): if policy_name not in self.policies: raise IAMNotFoundException( @@ -263,6 +280,16 @@ class User(BaseModel): self.access_keys.append(access_key) return access_key + def enable_mfa_device(self, + serial_number, + authentication_code_1, + authentication_code_2): + self.mfa_devices[serial_number] = MFADevice( + serial_number, + authentication_code_1, + authentication_code_2 + ) + def get_all_access_keys(self): return self.access_keys @@ -724,6 +751,39 @@ class IAMBackend(BaseBackend): user = self.get_user(user_name) user.delete_access_key(access_key_id) + def enable_mfa_device(self, + user_name, + serial_number, + authentication_code_1, + authentication_code_2): + """Enable MFA Device for user.""" + user = self.get_user(user_name) + if serial_number in user.mfa_devices: + raise IAMConflictException( + "EntityAlreadyExists", + "Device {0} already exists".format(serial_number) + ) + + user.enable_mfa_device( + serial_number, + authentication_code_1, + authentication_code_2 + ) + + def deactivate_mfa_device(self, user_name, serial_number): + """Deactivate and detach MFA Device from user if device exists.""" + user = self.get_user(user_name) + if serial_number not in user.mfa_devices: + raise IAMNotFoundException( + "Device {0} not found".format(serial_number) + ) + + user.deactivate_mfa_device(serial_number) + + def list_mfa_devices(self, user_name): + user = self.get_user(user_name) + return user.mfa_devices.values() + 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 27e69537d..0757d7eee 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -326,6 +326,35 @@ class IamResponse(BaseResponse): template = self.response_template(GENERIC_EMPTY_TEMPLATE) return template.render(name='DeleteAccessKey') + def deactivate_mfa_device(self): + user_name = self._get_param('UserName') + serial_number = self._get_param('SerialNumber') + + iam_backend.deactivate_mfa_device(user_name, serial_number) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name='DeactivateMFADevice') + + def enable_mfa_device(self): + user_name = self._get_param('UserName') + serial_number = self._get_param('SerialNumber') + authentication_code_1 = self._get_param('AuthenticationCode1') + authentication_code_2 = self._get_param('AuthenticationCode2') + + iam_backend.enable_mfa_device( + user_name, + serial_number, + authentication_code_1, + authentication_code_2 + ) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name='EnableMFADevice') + + def list_mfa_devices(self): + user_name = self._get_param('UserName') + devices = iam_backend.list_mfa_devices(user_name) + template = self.response_template(LIST_MFA_DEVICES_TEMPLATE) + return template.render(user_name=user_name, devices=devices) + def delete_user(self): user_name = self._get_param('UserName') iam_backend.delete_user(user_name) @@ -922,3 +951,20 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """6a8c3992-99f4-11e1-a4c3-27EXAMPLE804 """ + +LIST_MFA_DEVICES_TEMPLATE = """ + + + {% for device in devices %} + + {{ user_name }} + {{ device.serial_number }} + + {% endfor %} + + false + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 021d7c041..1ae892f62 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -292,6 +292,29 @@ def test_delete_access_key(): conn.delete_access_key(access_key_id, 'my-user') +@mock_iam() +def test_mfa_devices(): + # Test enable device + conn = boto3.client('iam', region_name='us-east-1') + conn.create_user(UserName='my-user') + conn.enable_mfa_device( + UserName='my-user', + SerialNumber='123456789', + AuthenticationCode1='234567', + AuthenticationCode2='987654' + ) + + # Test list mfa devices + response = conn.list_mfa_devices(UserName='my-user') + device = response['MFADevices'][0] + device['SerialNumber'].should.equal('123456789') + + # Test deactivate mfa device + conn.deactivate_mfa_device(UserName='my-user', SerialNumber='123456789') + response = conn.list_mfa_devices(UserName='my-user') + len(response['MFADevices']).should.equal(0) + + @mock_iam_deprecated() def test_delete_user(): conn = boto.connect_iam()