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.

311 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 datetime import datetime
  5. from mock import MagicMock, patch
  6. from odoo.http import Response
  7. from odoo.tests.common import TransactionCase
  8. from ..controllers.main import AuthTotp
  9. CONTROLLER_PATH = 'odoo.addons.auth_totp.controllers.main'
  10. REQUEST_PATH = CONTROLLER_PATH + '.request'
  11. SUPER_PATH = CONTROLLER_PATH + '.Home.web_login'
  12. JSON_PATH = CONTROLLER_PATH + '.JsonSecureCookie'
  13. RESPONSE_PATH = CONTROLLER_PATH + '.Response'
  14. DATETIME_PATH = CONTROLLER_PATH + '.datetime'
  15. REDIRECT_PATH = CONTROLLER_PATH + '.http.redirect_with_hash'
  16. TRANSLATE_PATH_CONT = CONTROLLER_PATH + '._'
  17. MODEL_PATH = 'odoo.addons.auth_totp.models.res_users'
  18. VALIDATE_PATH = MODEL_PATH + '.ResUsers.validate_mfa_confirmation_code'
  19. class AssignableDict(dict):
  20. pass
  21. @patch(REQUEST_PATH)
  22. class TestAuthTotp(TransactionCase):
  23. def setUp(self):
  24. super(TestAuthTotp, self).setUp()
  25. self.test_controller = AuthTotp()
  26. self.test_user = self.env.ref('base.user_root')
  27. self.test_user.mfa_enabled = False
  28. self.test_user.authenticator_ids = False
  29. self.env['res.users.authenticator'].create({
  30. 'name': 'Test Authenticator',
  31. 'secret_key': 'iamatestsecretyo',
  32. 'user_id': self.test_user.id,
  33. })
  34. self.test_user.mfa_enabled = True
  35. # Needed when tests are run with no prior requests (e.g. on a new DB)
  36. patcher = patch('odoo.http.request')
  37. self.addCleanup(patcher.stop)
  38. patcher.start()
  39. @patch(SUPER_PATH)
  40. def test_web_login_mfa_needed(self, super_mock, request_mock):
  41. '''Should update session and redirect correctly if MFA login needed'''
  42. request_mock.session = {'mfa_login_needed': True}
  43. request_mock.params = {'redirect': 'Test Redir'}
  44. test_result = self.test_controller.web_login()
  45. super_mock.assert_called_once()
  46. self.assertIn('/auth_totp/login?redirect=Test+Redir', test_result.data)
  47. self.assertFalse(request_mock.session['mfa_login_needed'])
  48. @patch(SUPER_PATH)
  49. def test_web_login_mfa_not_needed(self, super_mock, request_mock):
  50. '''Should return result of calling super if MFA login not needed'''
  51. test_response = 'Test Response'
  52. super_mock.return_value = test_response
  53. request_mock.session = {}
  54. self.assertEqual(self.test_controller.web_login().data, test_response)
  55. def test_mfa_login_get(self, request_mock):
  56. '''Should render mfa_login template with correct context'''
  57. request_mock.render.return_value = 'Test Value'
  58. request_mock.reset_mock()
  59. self.test_controller.mfa_login_get()
  60. request_mock.render.assert_called_once_with(
  61. 'auth_totp.mfa_login',
  62. qcontext=request_mock.params,
  63. )
  64. @patch(TRANSLATE_PATH_CONT)
  65. def test_mfa_login_post_no_login(self, tl_mock, request_mock):
  66. '''Should redirect correctly if login missing from session'''
  67. request_mock.env = self.env
  68. request_mock.session = {}
  69. request_mock.params = {'redirect': 'Test Redir'}
  70. tl_mock.side_effect = lambda arg: arg
  71. tl_mock.reset_mock()
  72. test_result = self.test_controller.mfa_login_post()
  73. tl_mock.assert_called_once()
  74. self.assertIn('/web/login?redirect=Test+Redir', test_result.data)
  75. self.assertIn('&error=You+must+log+in', test_result.data)
  76. @patch(TRANSLATE_PATH_CONT)
  77. def test_mfa_login_post_invalid_login(self, tl_mock, request_mock):
  78. '''Should redirect correctly if invalid login in session'''
  79. request_mock.env = self.env
  80. request_mock.session = {'login': 'Invalid Login'}
  81. request_mock.params = {'redirect': 'Test Redir'}
  82. tl_mock.side_effect = lambda arg: arg
  83. tl_mock.reset_mock()
  84. test_result = self.test_controller.mfa_login_post()
  85. tl_mock.assert_called_once()
  86. self.assertIn('/web/login?redirect=Test+Redir', test_result.data)
  87. self.assertIn('&error=You+must+log+in', test_result.data)
  88. @patch(TRANSLATE_PATH_CONT)
  89. def test_mfa_login_post_invalid_conf_code(self, tl_mock, request_mock):
  90. '''Should return correct redirect if confirmation code is invalid'''
  91. request_mock.env = self.env
  92. request_mock.session = {'login': self.test_user.login}
  93. request_mock.params = {
  94. 'redirect': 'Test Redir',
  95. 'confirmation_code': 'Invalid Code',
  96. }
  97. tl_mock.side_effect = lambda arg: arg
  98. tl_mock.reset_mock()
  99. test_result = self.test_controller.mfa_login_post()
  100. tl_mock.assert_called_once()
  101. self.assertIn('/auth_totp/login?redirect=Test+Redir', test_result.data)
  102. self.assertIn(
  103. '&error=Your+confirmation+code+is+not+correct.',
  104. test_result.data,
  105. )
  106. @patch(VALIDATE_PATH)
  107. def test_mfa_login_post_valid_conf_code(self, val_mock, request_mock):
  108. '''Should correctly update session if confirmation code is valid'''
  109. request_mock.env = self.env
  110. request_mock.session = AssignableDict(login=self.test_user.login)
  111. request_mock.session.authenticate = MagicMock()
  112. test_conf_code = 'Test Code'
  113. request_mock.params = {'confirmation_code': test_conf_code}
  114. val_mock.return_value = True
  115. self.test_controller.mfa_login_post()
  116. val_mock.assert_called_once_with(test_conf_code)
  117. resulting_flag = request_mock.session['mfa_login_active']
  118. self.assertEqual(resulting_flag, self.test_user.id)
  119. @patch(VALIDATE_PATH)
  120. def test_mfa_login_post_pass_auth_fail(self, val_mock, request_mock):
  121. '''Should not set success param if password auth fails'''
  122. request_mock.env = self.env
  123. request_mock.db = test_db = 'Test DB'
  124. test_password = 'Test Password'
  125. request_mock.session = AssignableDict(
  126. login=self.test_user.login, password=test_password,
  127. )
  128. request_mock.session.authenticate = MagicMock(return_value=False)
  129. request_mock.params = {}
  130. val_mock.return_value = True
  131. self.test_controller.mfa_login_post()
  132. request_mock.session.authenticate.assert_called_once_with(
  133. test_db, self.test_user.login, test_password,
  134. )
  135. self.assertFalse(request_mock.params.get('login_success'))
  136. @patch(VALIDATE_PATH)
  137. def test_mfa_login_post_pass_auth_success(self, val_mock, request_mock):
  138. '''Should set success param if password auth succeeds'''
  139. request_mock.env = self.env
  140. request_mock.db = test_db = 'Test DB'
  141. test_password = 'Test Password'
  142. request_mock.session = AssignableDict(
  143. login=self.test_user.login, password=test_password,
  144. )
  145. request_mock.session.authenticate = MagicMock(return_value=True)
  146. request_mock.params = {}
  147. val_mock.return_value = True
  148. self.test_controller.mfa_login_post()
  149. request_mock.session.authenticate.assert_called_once_with(
  150. test_db, self.test_user.login, test_password,
  151. )
  152. self.assertTrue(request_mock.params.get('login_success'))
  153. @patch(VALIDATE_PATH)
  154. def test_mfa_login_post_redirect(self, val_mock, request_mock):
  155. '''Should return correct redirect if info valid and redirect present'''
  156. request_mock.env = self.env
  157. request_mock.session = AssignableDict(login=self.test_user.login)
  158. request_mock.session.authenticate = MagicMock(return_value=True)
  159. test_redir = 'Test Redir'
  160. request_mock.params = {'redirect': test_redir}
  161. val_mock.return_value = True
  162. test_result = self.test_controller.mfa_login_post()
  163. self.assertIn("window.location = '%s'" % test_redir, test_result.data)
  164. @patch(VALIDATE_PATH)
  165. def test_mfa_login_post_redir_def(self, val_mock, request_mock):
  166. '''Should return redirect to /web if info valid and no redirect'''
  167. request_mock.env = self.env
  168. request_mock.session = AssignableDict(login=self.test_user.login)
  169. request_mock.session.authenticate = MagicMock(return_value=True)
  170. request_mock.params = {}
  171. val_mock.return_value = True
  172. test_result = self.test_controller.mfa_login_post()
  173. self.assertIn("window.location = '/web'", test_result.data)
  174. @patch(RESPONSE_PATH)
  175. @patch(JSON_PATH)
  176. @patch(VALIDATE_PATH)
  177. def test_mfa_login_post_cookie_werkzeug_cookie(
  178. self, val_mock, json_mock, resp_mock, request_mock
  179. ):
  180. '''Should create Werkzeug cookie w/right info if remember flag set'''
  181. request_mock.env = self.env
  182. request_mock.session = AssignableDict(login=self.test_user.login)
  183. request_mock.session.authenticate = MagicMock(return_value=True)
  184. request_mock.params = {'remember_device': True}
  185. val_mock.return_value = True
  186. resp_mock().__class__ = Response
  187. json_mock.reset_mock()
  188. self.test_controller.mfa_login_post()
  189. test_secret = self.test_user.trusted_device_cookie_key
  190. json_mock.assert_called_once_with(
  191. {'user_id': self.test_user.id},
  192. test_secret,
  193. )
  194. @patch(DATETIME_PATH)
  195. @patch(RESPONSE_PATH)
  196. @patch(JSON_PATH)
  197. @patch(VALIDATE_PATH)
  198. def test_mfa_login_post_cookie_werkzeug_cookie_exp(
  199. self, val_mock, json_mock, resp_mock, dt_mock, request_mock
  200. ):
  201. '''Should serialize Werkzeug cookie w/right exp if remember flag set'''
  202. request_mock.env = self.env
  203. request_mock.session = AssignableDict(login=self.test_user.login)
  204. request_mock.session.authenticate = MagicMock(return_value=True)
  205. request_mock.params = {'remember_device': True}
  206. val_mock.return_value = True
  207. dt_mock.utcnow.return_value = datetime(2016, 12, 1)
  208. resp_mock().__class__ = Response
  209. json_mock.reset_mock()
  210. self.test_controller.mfa_login_post()
  211. json_mock().serialize.assert_called_once_with(datetime(2016, 12, 31))
  212. @patch(DATETIME_PATH)
  213. @patch(RESPONSE_PATH)
  214. @patch(JSON_PATH)
  215. @patch(VALIDATE_PATH)
  216. def test_mfa_login_post_cookie_final_cookie(
  217. self, val_mock, json_mock, resp_mock, dt_mock, request_mock
  218. ):
  219. '''Should add correct cookie to response if remember flag set'''
  220. request_mock.env = self.env
  221. request_mock.session = AssignableDict(login=self.test_user.login)
  222. request_mock.session.authenticate = MagicMock(return_value=True)
  223. request_mock.params = {'remember_device': True}
  224. val_mock.return_value = True
  225. dt_mock.utcnow.return_value = datetime(2016, 12, 1)
  226. config_model = self.env['ir.config_parameter']
  227. config_model.set_param('auth_totp.secure_cookie', '0')
  228. resp_mock().__class__ = Response
  229. resp_mock.reset_mock()
  230. self.test_controller.mfa_login_post()
  231. resp_mock().set_cookie.assert_called_once_with(
  232. 'trusted_devices_%s' % self.test_user.id,
  233. json_mock().serialize(),
  234. max_age=30 * 24 * 60 * 60,
  235. expires=datetime(2016, 12, 31),
  236. httponly=True,
  237. secure=False,
  238. )
  239. @patch(RESPONSE_PATH)
  240. @patch(VALIDATE_PATH)
  241. def test_mfa_login_post_cookie_final_cookie_secure(
  242. self, val_mock, resp_mock, request_mock
  243. ):
  244. '''Should set secure cookie if config parameter set accordingly'''
  245. request_mock.env = self.env
  246. request_mock.session = AssignableDict(login=self.test_user.login)
  247. request_mock.session.authenticate = MagicMock(return_value=True)
  248. request_mock.params = {'remember_device': True}
  249. val_mock.return_value = True
  250. config_model = self.env['ir.config_parameter']
  251. config_model.set_param('auth_totp.secure_cookie', '1')
  252. resp_mock().__class__ = Response
  253. resp_mock.reset_mock()
  254. self.test_controller.mfa_login_post()
  255. new_test_security = resp_mock().set_cookie.mock_calls[0][2]['secure']
  256. self.assertIs(new_test_security, True)
  257. @patch(REDIRECT_PATH)
  258. @patch(VALIDATE_PATH)
  259. def test_mfa_login_post_firefox_response_returned(
  260. self, val_mock, redirect_mock, request_mock
  261. ):
  262. '''Should behave well if redirect returns Response (Firefox case)'''
  263. request_mock.env = self.env
  264. request_mock.session = AssignableDict(login=self.test_user.login)
  265. request_mock.session.authenticate = MagicMock(return_value=True)
  266. redirect_mock.return_value = Response('Test Response')
  267. val_mock.return_value = True
  268. test_result = self.test_controller.mfa_login_post()
  269. self.assertIn('Test Response', test_result.response)