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.

277 lines
12 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 mock import patch
  5. from odoo.exceptions import AccessDenied, ValidationError
  6. from odoo.tests.common import TransactionCase
  7. from ..exceptions import MfaLoginNeeded
  8. from ..models.res_users import JsonSecureCookie
  9. from ..models.res_users_authenticator import ResUsersAuthenticator
  10. MODEL_PATH = 'odoo.addons.auth_totp.models.res_users'
  11. REQUEST_PATH = MODEL_PATH + '.request'
  12. class TestResUsers(TransactionCase):
  13. def setUp(self):
  14. super(TestResUsers, self).setUp()
  15. self.test_model = self.env['res.users']
  16. self.test_user = self.env.ref('base.user_root')
  17. self.test_user.mfa_enabled = False
  18. self.test_user.authenticator_ids = False
  19. self.env['res.users.authenticator'].create({
  20. 'name': 'Test Name',
  21. 'secret_key': 'Test Key',
  22. 'user_id': self.test_user.id,
  23. })
  24. self.test_user.mfa_enabled = True
  25. self.env.uid = self.test_user.id
  26. def test_compute_trusted_device_cookie_key_disable_mfa(self):
  27. """It should clear out existing key when MFA is disabled"""
  28. self.test_user.mfa_enabled = False
  29. self.assertFalse(self.test_user.trusted_device_cookie_key)
  30. def test_compute_trusted_device_cookie_key_enable_mfa(self):
  31. """It should generate a new key when MFA is enabled"""
  32. old_key = self.test_user.trusted_device_cookie_key
  33. self.test_user.mfa_enabled = False
  34. self.test_user.mfa_enabled = True
  35. self.assertNotEqual(self.test_user.trusted_device_cookie_key, old_key)
  36. def test_build_model_mfa_fields_in_self_writeable_list(self):
  37. '''Should add MFA fields to list of fields users can modify for self'''
  38. ResUsersClass = type(self.test_user)
  39. self.assertIn('mfa_enabled', ResUsersClass.SELF_WRITEABLE_FIELDS)
  40. self.assertIn('authenticator_ids', ResUsersClass.SELF_WRITEABLE_FIELDS)
  41. def test_check_enabled_with_authenticator_mfa_no_auth(self):
  42. '''Should raise correct error if MFA enabled without authenticators'''
  43. with self.assertRaisesRegexp(ValidationError, 'locked out'):
  44. self.test_user.authenticator_ids = False
  45. def test_check_enabled_with_authenticator_no_mfa_auth(self):
  46. '''Should not raise error if MFA not enabled with authenticators'''
  47. try:
  48. self.test_user.mfa_enabled = False
  49. except ValidationError:
  50. self.fail('A ValidationError was raised and should not have been.')
  51. @patch(REQUEST_PATH, new=None)
  52. def test_check_mfa_without_request(self):
  53. """It should remove UID from cache if in MFA cache and no request"""
  54. test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
  55. test_cache[self.env.uid] = 'test'
  56. self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
  57. try:
  58. self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
  59. except AccessDenied:
  60. pass
  61. self.assertFalse(test_cache.get(self.env.uid))
  62. @patch(REQUEST_PATH)
  63. def test_check_mfa_no_mfa_session(self, request_mock):
  64. """It should remove UID from cache if MFA cache but no MFA session"""
  65. request_mock.session = {}
  66. test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
  67. test_cache[self.env.uid] = 'test'
  68. self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
  69. try:
  70. self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
  71. except AccessDenied:
  72. pass
  73. self.assertFalse(test_cache.get(self.env.uid))
  74. @patch(REQUEST_PATH)
  75. def test_check_mfa_invalid_mfa_session(self, request_mock):
  76. """It should remove UID if in MFA cache but invalid MFA session"""
  77. request_mock.session = {'mfa_login_active': self.env.uid + 1}
  78. test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
  79. test_cache[self.env.uid] = 'test'
  80. self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
  81. try:
  82. self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
  83. except AccessDenied:
  84. pass
  85. self.assertFalse(test_cache.get(self.env.uid))
  86. def test_check_no_mfa(self):
  87. """It should not remove UID from cache if not in MFA cache"""
  88. test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
  89. test_cache[self.env.uid] = 'test'
  90. self.test_model._mfa_uid_cache[self.env.cr.dbname].clear()
  91. self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
  92. self.assertEqual(test_cache.get(self.env.uid), 'test')
  93. @patch(REQUEST_PATH)
  94. def test_check_mfa_valid_session(self, request_mock):
  95. """It should not remove UID if in MFA cache and valid session"""
  96. request_mock.session = {'mfa_login_active': self.env.uid}
  97. test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
  98. test_cache[self.env.uid] = 'test'
  99. self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
  100. self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
  101. self.assertEqual(test_cache.get(self.env.uid), 'test')
  102. def test_check_credentials_mfa_not_enabled(self):
  103. '''Should check password if user does not have MFA enabled'''
  104. self.test_user.mfa_enabled = False
  105. with self.assertRaises(AccessDenied):
  106. self.env['res.users'].check_credentials('invalid')
  107. try:
  108. self.env['res.users'].check_credentials('admin')
  109. except AccessDenied:
  110. self.fail('An exception was raised with a correct password.')
  111. def test_check_credentials_mfa_uid_cache(self):
  112. """It should add user's ID to MFA UID cache if MFA enabled"""
  113. self.test_model._mfa_uid_cache[self.env.cr.dbname].clear()
  114. try:
  115. self.test_model.check_credentials('invalid')
  116. except AccessDenied:
  117. pass
  118. result_cache = self.test_model._mfa_uid_cache[self.env.cr.dbname]
  119. self.assertEqual(result_cache, {self.test_user.id})
  120. @patch(REQUEST_PATH, new=None)
  121. def test_check_credentials_mfa_and_no_request(self):
  122. '''Should raise correct exception if MFA enabled and no request'''
  123. with self.assertRaises(AccessDenied):
  124. self.env['res.users'].check_credentials('invalid')
  125. with self.assertRaises(MfaLoginNeeded):
  126. self.env['res.users'].check_credentials('admin')
  127. @patch(REQUEST_PATH)
  128. def test_check_credentials_mfa_login_active(self, request_mock):
  129. '''Should check password if user has finished MFA auth this session'''
  130. request_mock.session = {'mfa_login_active': self.test_user.id}
  131. with self.assertRaises(AccessDenied):
  132. self.env['res.users'].check_credentials('invalid')
  133. try:
  134. self.env['res.users'].check_credentials('admin')
  135. except AccessDenied:
  136. self.fail('An exception was raised with a correct password.')
  137. @patch(REQUEST_PATH)
  138. def test_check_credentials_mfa_different_login_active(self, request_mock):
  139. '''Should correctly raise/update if other user finished MFA auth'''
  140. request_mock.session = {'mfa_login_active': self.test_user.id + 1}
  141. request_mock.httprequest.cookies = {}
  142. with self.assertRaises(AccessDenied):
  143. self.env['res.users'].check_credentials('invalid')
  144. self.assertFalse(request_mock.session.get('mfa_login_needed'))
  145. with self.assertRaises(MfaLoginNeeded):
  146. self.env['res.users'].check_credentials('admin')
  147. self.assertTrue(request_mock.session.get('mfa_login_needed'))
  148. @patch(REQUEST_PATH)
  149. def test_check_credentials_mfa_no_device_cookie(self, request_mock):
  150. '''Should correctly raise/update session if MFA and no device cookie'''
  151. request_mock.session = {'mfa_login_active': False}
  152. request_mock.httprequest.cookies = {}
  153. with self.assertRaises(AccessDenied):
  154. self.env['res.users'].check_credentials('invalid')
  155. self.assertFalse(request_mock.session.get('mfa_login_needed'))
  156. with self.assertRaises(MfaLoginNeeded):
  157. self.env['res.users'].check_credentials('admin')
  158. self.assertTrue(request_mock.session.get('mfa_login_needed'))
  159. @patch(REQUEST_PATH)
  160. def test_check_credentials_mfa_corrupted_device_cookie(self, request_mock):
  161. '''Should correctly raise/update session if MFA and corrupted cookie'''
  162. request_mock.session = {'mfa_login_active': False}
  163. test_key = 'trusted_devices_%d' % self.test_user.id
  164. request_mock.httprequest.cookies = {test_key: 'invalid'}
  165. with self.assertRaises(AccessDenied):
  166. self.env['res.users'].check_credentials('invalid')
  167. self.assertFalse(request_mock.session.get('mfa_login_needed'))
  168. with self.assertRaises(MfaLoginNeeded):
  169. self.env['res.users'].check_credentials('admin')
  170. self.assertTrue(request_mock.session.get('mfa_login_needed'))
  171. @patch(REQUEST_PATH)
  172. def test_check_credentials_mfa_cookie_from_wrong_user(self, request_mock):
  173. '''Should raise and update session if MFA and wrong user's cookie'''
  174. request_mock.session = {'mfa_login_active': False}
  175. test_user_2 = self.env['res.users'].create({
  176. 'name': 'Test User',
  177. 'login': 'test_user',
  178. })
  179. test_id_2 = test_user_2.id
  180. self.env['res.users.authenticator'].create({
  181. 'name': 'Test Name',
  182. 'secret_key': 'Test Key',
  183. 'user_id': test_id_2,
  184. })
  185. test_user_2.mfa_enabled = True
  186. secret = test_user_2.trusted_device_cookie_key
  187. test_device_cookie = JsonSecureCookie({'user_id': test_id_2}, secret)
  188. test_device_cookie = test_device_cookie.serialize()
  189. test_key = 'trusted_devices_%d' % self.test_user.id
  190. request_mock.httprequest.cookies = {test_key: test_device_cookie}
  191. with self.assertRaises(AccessDenied):
  192. self.env['res.users'].check_credentials('invalid')
  193. self.assertFalse(request_mock.session.get('mfa_login_needed'))
  194. with self.assertRaises(MfaLoginNeeded):
  195. self.env['res.users'].check_credentials('admin')
  196. self.assertTrue(request_mock.session.get('mfa_login_needed'))
  197. @patch(REQUEST_PATH)
  198. def test_check_credentials_mfa_correct_device_cookie(self, request_mock):
  199. '''Should check password if MFA and correct device cookie'''
  200. request_mock.session = {'mfa_login_active': False}
  201. secret = self.test_user.trusted_device_cookie_key
  202. test_device_cookie = JsonSecureCookie(
  203. {'user_id': self.test_user.id},
  204. secret,
  205. )
  206. test_device_cookie = test_device_cookie.serialize()
  207. test_key = 'trusted_devices_%d' % self.test_user.id
  208. request_mock.httprequest.cookies = {test_key: test_device_cookie}
  209. with self.assertRaises(AccessDenied):
  210. self.env['res.users'].check_credentials('invalid')
  211. try:
  212. self.env['res.users'].check_credentials('admin')
  213. except AccessDenied:
  214. self.fail('An exception was raised with a correct password.')
  215. def test_validate_mfa_confirmation_code_not_singleton(self):
  216. '''Should raise correct error when recordset is not singleton'''
  217. test_user_2 = self.env['res.users']
  218. test_user_3 = self.env.ref('base.public_user')
  219. test_set = self.test_user + test_user_3
  220. with self.assertRaisesRegexp(ValueError, 'Expected singleton'):
  221. test_user_2.validate_mfa_confirmation_code('Test Code')
  222. with self.assertRaisesRegexp(ValueError, 'Expected singleton'):
  223. test_set.validate_mfa_confirmation_code('Test Code')
  224. @patch.object(ResUsersAuthenticator, 'validate_conf_code')
  225. def test_validate_mfa_confirmation_code_singleton_return(self, mock_func):
  226. '''Should return validate_conf_code() value if singleton recordset'''
  227. mock_func.return_value = 'Test Result'
  228. self.assertEqual(
  229. self.test_user.validate_mfa_confirmation_code('Test Code'),
  230. 'Test Result',
  231. )