You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
8.6 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016-2017 LasLabs Inc.
  3. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
  4. from datetime import datetime
  5. import mock
  6. import string
  7. from odoo.exceptions import ValidationError
  8. from odoo.tests.common import TransactionCase
  9. from ..exceptions import (
  10. MfaTokenError,
  11. MfaTokenInvalidError,
  12. MfaTokenExpiredError,
  13. )
  14. from ..models.res_users_authenticator import ResUsersAuthenticator
  15. DATETIME_PATH = 'odoo.addons.auth_totp.models.res_users.datetime'
  16. class TestResUsers(TransactionCase):
  17. def setUp(self):
  18. super(TestResUsers, self).setUp()
  19. self.test_user = self.env.ref('base.user_root')
  20. self.test_user.mfa_enabled = False
  21. self.test_user.authenticator_ids = False
  22. self.env.uid = self.test_user.id
  23. def test_build_model_mfa_fields_in_self_writeable_list(self):
  24. '''Should add MFA fields to list of fields users can modify for self'''
  25. ResUsersClass = type(self.test_user)
  26. self.assertIn('mfa_enabled', ResUsersClass.SELF_WRITEABLE_FIELDS)
  27. self.assertIn('authenticator_ids', ResUsersClass.SELF_WRITEABLE_FIELDS)
  28. def test_check_enabled_with_authenticator_mfa_no_auth(self):
  29. '''Should raise correct error if MFA enabled without authenticators'''
  30. with self.assertRaisesRegexp(ValidationError, 'locked out'):
  31. self.test_user.mfa_enabled = True
  32. def test_check_enabled_with_authenticator_no_mfa_auth(self):
  33. '''Should not raise error if MFA not enabled with authenticators'''
  34. try:
  35. self.env['res.users.authenticator'].create({
  36. 'name': 'Test Name',
  37. 'secret_key': 'Test Key',
  38. 'user_id': self.test_user.id,
  39. })
  40. except ValidationError:
  41. self.fail('A ValidationError was raised and should not have been.')
  42. def test_check_enabled_with_authenticator_mfa_auth(self):
  43. '''Should not raise error if MFA enabled with authenticators'''
  44. try:
  45. self.env['res.users.authenticator'].create({
  46. 'name': 'Test Name',
  47. 'secret_key': 'Test Key',
  48. 'user_id': self.test_user.id,
  49. })
  50. self.test_user.mfa_enabled = True
  51. except ValidationError:
  52. self.fail('A ValidationError was raised and should not have been.')
  53. def test_check_credentials_no_match(self):
  54. '''Should raise appropriate error if there is no match'''
  55. with self.assertRaises(MfaTokenInvalidError):
  56. self.env['res.users'].check_credentials('invalid')
  57. @mock.patch(DATETIME_PATH)
  58. def test_check_credentials_expired(self, datetime_mock):
  59. '''Should raise appropriate error if match based on expired token'''
  60. datetime_mock.now.return_value = datetime(2016, 12, 1)
  61. self.test_user.generate_mfa_login_token()
  62. test_token = self.test_user.mfa_login_token
  63. datetime_mock.now.return_value = datetime(2017, 12, 1)
  64. with self.assertRaises(MfaTokenExpiredError):
  65. self.env['res.users'].check_credentials(test_token)
  66. def test_check_credentials_current(self):
  67. '''Should not raise error if match based on active token'''
  68. self.test_user.generate_mfa_login_token()
  69. test_token = self.test_user.mfa_login_token
  70. try:
  71. self.env['res.users'].check_credentials(test_token)
  72. except MfaTokenError:
  73. self.fail('An MfaTokenError was raised and should not have been.')
  74. def test_generate_mfa_login_token_token_field_content(self):
  75. '''Should set token field to 20 char string of ASCII letters/digits'''
  76. self.test_user.generate_mfa_login_token()
  77. test_chars = set(string.ascii_letters + string.digits)
  78. self.assertEqual(len(self.test_user.mfa_login_token), 20)
  79. self.assertTrue(set(self.test_user.mfa_login_token) <= test_chars)
  80. def test_generate_mfa_login_token_token_field_random(self):
  81. '''Should set token field to new value each time'''
  82. test_tokens = set([])
  83. for __ in xrange(3):
  84. self.test_user.generate_mfa_login_token()
  85. test_tokens.add(self.test_user.mfa_login_token)
  86. self.assertEqual(len(test_tokens), 3)
  87. @mock.patch(DATETIME_PATH)
  88. def test_generate_mfa_login_token_exp_field_default(self, datetime_mock):
  89. '''Should set token lifetime to 15 minutes if no argument provided'''
  90. datetime_mock.now.return_value = datetime(2016, 12, 1)
  91. self.test_user.generate_mfa_login_token()
  92. self.assertEqual(
  93. self.test_user.mfa_login_token_exp,
  94. '2016-12-01 00:15:00'
  95. )
  96. @mock.patch(DATETIME_PATH)
  97. def test_generate_mfa_login_token_exp_field_custom(self, datetime_mock):
  98. '''Should set token lifetime to value provided'''
  99. datetime_mock.now.return_value = datetime(2016, 12, 1)
  100. self.test_user.generate_mfa_login_token(45)
  101. self.assertEqual(
  102. self.test_user.mfa_login_token_exp,
  103. '2016-12-01 00:45:00'
  104. )
  105. def test_user_from_mfa_login_token_validate_not_singleton(self):
  106. '''Should raise correct error when recordset is not a singleton'''
  107. self.test_user.copy()
  108. test_set = self.env['res.users'].search([('id', '>', 0)], limit=2)
  109. with self.assertRaises(MfaTokenInvalidError):
  110. self.env['res.users']._user_from_mfa_login_token_validate()
  111. with self.assertRaises(MfaTokenInvalidError):
  112. test_set._user_from_mfa_login_token_validate()
  113. @mock.patch(DATETIME_PATH)
  114. def test_user_from_mfa_login_token_validate_expired(self, datetime_mock):
  115. '''Should raise correct error when record has expired token'''
  116. datetime_mock.now.return_value = datetime(2016, 12, 1)
  117. self.test_user.generate_mfa_login_token()
  118. datetime_mock.now.return_value = datetime(2017, 12, 1)
  119. with self.assertRaises(MfaTokenExpiredError):
  120. self.test_user._user_from_mfa_login_token_validate()
  121. def test_user_from_mfa_login_token_validate_current_singleton(self):
  122. '''Should not raise error when one record with active token'''
  123. self.test_user.generate_mfa_login_token()
  124. try:
  125. self.test_user._user_from_mfa_login_token_validate()
  126. except MfaTokenError:
  127. self.fail('An MfaTokenError was raised and should not have been.')
  128. def test_user_from_mfa_login_token_match(self):
  129. '''Should retreive correct user when there is a current match'''
  130. self.test_user.generate_mfa_login_token()
  131. test_token = self.test_user.mfa_login_token
  132. self.assertEqual(
  133. self.env['res.users'].user_from_mfa_login_token(test_token),
  134. self.test_user,
  135. )
  136. def test_user_from_mfa_login_token_falsy(self):
  137. '''Should raise correct error when token is falsy'''
  138. with self.assertRaises(MfaTokenInvalidError):
  139. self.env['res.users'].user_from_mfa_login_token(None)
  140. def test_user_from_mfa_login_token_no_match(self):
  141. '''Should raise correct error when there is no match'''
  142. with self.assertRaises(MfaTokenInvalidError):
  143. self.env['res.users'].user_from_mfa_login_token('Test Token')
  144. @mock.patch(DATETIME_PATH)
  145. def test_user_from_mfa_login_token_match_expired(self, datetime_mock):
  146. '''Should raise correct error when the match is expired'''
  147. datetime_mock.now.return_value = datetime(2016, 12, 1)
  148. self.test_user.generate_mfa_login_token()
  149. test_token = self.test_user.mfa_login_token
  150. datetime_mock.now.return_value = datetime(2017, 12, 1)
  151. with self.assertRaises(MfaTokenExpiredError):
  152. self.env['res.users'].user_from_mfa_login_token(test_token)
  153. def test_validate_mfa_confirmation_code_not_singleton(self):
  154. '''Should raise correct error when recordset is not singleton'''
  155. test_user_2 = self.env['res.users']
  156. test_user_3 = self.env.ref('base.public_user')
  157. test_set = self.test_user + test_user_3
  158. with self.assertRaisesRegexp(ValueError, 'Expected singleton'):
  159. test_user_2.validate_mfa_confirmation_code('Test Code')
  160. with self.assertRaisesRegexp(ValueError, 'Expected singleton'):
  161. test_set.validate_mfa_confirmation_code('Test Code')
  162. @mock.patch.object(ResUsersAuthenticator, 'validate_conf_code')
  163. def test_validate_mfa_confirmation_code_singleton_return(self, mock_func):
  164. '''Should return validate_conf_code() value if singleton recordset'''
  165. mock_func.return_value = 'Test Result'
  166. self.assertEqual(
  167. self.test_user.validate_mfa_confirmation_code('Test Code'),
  168. 'Test Result',
  169. )