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.

411 lines
16 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. 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. ENVIRONMENT_PATH = CONTROLLER_PATH + '.Environment'
  14. RESPONSE_PATH = CONTROLLER_PATH + '.Response'
  15. DATETIME_PATH = CONTROLLER_PATH + '.datetime'
  16. REDIRECT_PATH = CONTROLLER_PATH + '.http.redirect_with_hash'
  17. TRANSLATE_PATH_CONT = CONTROLLER_PATH + '._'
  18. MODEL_PATH = 'odoo.addons.auth_totp.models.res_users'
  19. GENERATE_PATH = MODEL_PATH + '.ResUsers.generate_mfa_login_token'
  20. VALIDATE_PATH = MODEL_PATH + '.ResUsers.validate_mfa_confirmation_code'
  21. TRANSLATE_PATH_MOD = MODEL_PATH + '._'
  22. @mock.patch(REQUEST_PATH)
  23. class TestAuthTotp(TransactionCase):
  24. def setUp(self):
  25. super(TestAuthTotp, self).setUp()
  26. self.test_controller = AuthTotp()
  27. self.test_user = self.env.ref('base.user_root')
  28. self.env['res.users.authenticator'].create({
  29. 'name': 'Test Authenticator',
  30. 'secret_key': 'iamatestsecretyo',
  31. 'user_id': self.test_user.id,
  32. })
  33. self.test_user.mfa_enabled = True
  34. self.test_user.generate_mfa_login_token()
  35. self.test_user.trusted_device_ids = None
  36. # Needed when tests are run with no prior requests (e.g. on a new DB)
  37. patcher = mock.patch('odoo.http.request')
  38. self.addCleanup(patcher.stop)
  39. patcher.start()
  40. @mock.patch(SUPER_PATH)
  41. def test_web_login_no_password_login(self, super_mock, request_mock):
  42. '''Should return wrapped result of super if no password log in'''
  43. test_response = 'Test Response'
  44. super_mock.return_value = test_response
  45. request_mock.params = {}
  46. self.assertEqual(self.test_controller.web_login().data, test_response)
  47. @mock.patch(SUPER_PATH)
  48. def test_web_login_user_no_mfa(self, super_mock, request_mock):
  49. '''Should return wrapped result of super if user did not enable MFA'''
  50. test_response = 'Test Response'
  51. super_mock.return_value = test_response
  52. request_mock.params = {'login_success': True}
  53. request_mock.env = self.env
  54. request_mock.uid = self.test_user.id
  55. self.test_user.mfa_enabled = False
  56. self.assertEqual(self.test_controller.web_login().data, test_response)
  57. @mock.patch(JSON_PATH)
  58. @mock.patch(SUPER_PATH)
  59. def test_web_login_valid_cookie(self, super_mock, json_mock, request_mock):
  60. '''Should return wrapped result of super if valid device cookie'''
  61. test_response = 'Test Response'
  62. super_mock.return_value = test_response
  63. request_mock.params = {'login_success': True}
  64. request_mock.env = self.env
  65. request_mock.uid = self.test_user.id
  66. device_model = self.env['res.users.device']
  67. test_device = device_model.create({'user_id': self.test_user.id})
  68. json_mock.unserialize().get.return_value = test_device.id
  69. self.assertEqual(self.test_controller.web_login().data, test_response)
  70. @mock.patch(SUPER_PATH)
  71. @mock.patch(GENERATE_PATH)
  72. def test_web_login_no_cookie(self, gen_mock, super_mock, request_mock):
  73. '''Should respond correctly if no device cookie with expected key'''
  74. request_mock.env = self.env
  75. request_mock.uid = self.test_user.id
  76. request_mock.params = {
  77. 'login_success': True,
  78. 'redirect': 'Test Redir',
  79. }
  80. self.test_user.mfa_login_token = 'Test Token'
  81. request_mock.httprequest.cookies = {}
  82. request_mock.reset_mock()
  83. test_result = self.test_controller.web_login()
  84. gen_mock.assert_called_once_with()
  85. request_mock.session.logout.assert_called_once_with(keep_db=True)
  86. self.assertIn(
  87. '/auth_totp/login?redirect=Test+Redir&mfa_login_token=Test+Token',
  88. test_result.data,
  89. )
  90. @mock.patch(SUPER_PATH)
  91. @mock.patch(JSON_PATH)
  92. @mock.patch(GENERATE_PATH)
  93. def test_web_login_bad_device_id(
  94. self, gen_mock, json_mock, super_mock, request_mock
  95. ):
  96. '''Should respond correctly if invalid device_id in device cookie'''
  97. request_mock.env = self.env
  98. request_mock.uid = self.test_user.id
  99. request_mock.params = {
  100. 'login_success': True,
  101. 'redirect': 'Test Redir',
  102. }
  103. self.test_user.mfa_login_token = 'Test Token'
  104. json_mock.unserialize.return_value = {'device_id': 1}
  105. request_mock.reset_mock()
  106. test_result = self.test_controller.web_login()
  107. gen_mock.assert_called_once_with()
  108. request_mock.session.logout.assert_called_once_with(keep_db=True)
  109. self.assertIn(
  110. '/auth_totp/login?redirect=Test+Redir&mfa_login_token=Test+Token',
  111. test_result.data,
  112. )
  113. def test_mfa_login_get(self, request_mock):
  114. '''Should render mfa_login template with correct context'''
  115. request_mock.render.return_value = 'Test Value'
  116. request_mock.reset_mock()
  117. self.test_controller.mfa_login_get()
  118. request_mock.render.assert_called_once_with(
  119. 'auth_totp.mfa_login',
  120. qcontext=request_mock.params,
  121. )
  122. @mock.patch(TRANSLATE_PATH_MOD)
  123. def test_mfa_login_post_invalid_token(self, tl_mock, request_mock):
  124. '''Should return correct redirect if login token invalid'''
  125. request_mock.env = self.env
  126. request_mock.params = {
  127. 'mfa_login_token': 'Invalid Token',
  128. 'redirect': 'Test Redir',
  129. }
  130. tl_mock.side_effect = lambda arg: arg
  131. tl_mock.reset_mock()
  132. test_result = self.test_controller.mfa_login_post()
  133. tl_mock.assert_called_once()
  134. self.assertIn('/web/login?redirect=Test+Redir', test_result.data)
  135. self.assertIn(
  136. '&error=Your+MFA+login+token+is+not+valid.',
  137. test_result.data,
  138. )
  139. @mock.patch(TRANSLATE_PATH_MOD)
  140. def test_mfa_login_post_expired_token(self, tl_mock, request_mock):
  141. '''Should return correct redirect if login token expired'''
  142. request_mock.env = self.env
  143. self.test_user.generate_mfa_login_token(-1)
  144. request_mock.params = {
  145. 'mfa_login_token': self.test_user.mfa_login_token,
  146. 'redirect': 'Test Redir',
  147. }
  148. tl_mock.side_effect = lambda arg: arg
  149. tl_mock.reset_mock()
  150. test_result = self.test_controller.mfa_login_post()
  151. tl_mock.assert_called_once()
  152. self.assertIn('/web/login?redirect=Test+Redir', test_result.data)
  153. self.assertIn(
  154. '&error=Your+MFA+login+token+has+expired.',
  155. test_result.data,
  156. )
  157. @mock.patch(TRANSLATE_PATH_CONT)
  158. def test_mfa_login_post_invalid_conf_code(self, tl_mock, request_mock):
  159. '''Should return correct redirect if confirmation code is invalid'''
  160. request_mock.env = self.env
  161. request_mock.params = {
  162. 'mfa_login_token': self.test_user.mfa_login_token,
  163. 'redirect': 'Test Redir',
  164. 'confirmation_code': 'Invalid Code',
  165. }
  166. tl_mock.side_effect = lambda arg: arg
  167. tl_mock.reset_mock()
  168. test_result = self.test_controller.mfa_login_post()
  169. tl_mock.assert_called_once()
  170. self.assertIn('/auth_totp/login?redirect=Test+Redir', test_result.data)
  171. self.assertIn(
  172. '&error=Your+confirmation+code+is+not+correct.',
  173. test_result.data,
  174. )
  175. self.assertIn(
  176. '&mfa_login_token=%s' % self.test_user.mfa_login_token,
  177. test_result.data,
  178. )
  179. @mock.patch(GENERATE_PATH)
  180. @mock.patch(VALIDATE_PATH)
  181. def test_mfa_login_post_new_token(self, val_mock, gen_mock, request_mock):
  182. '''Should refresh user's login token w/right lifetime if info valid'''
  183. request_mock.env = self.env
  184. request_mock.db = self.registry.db_name
  185. test_token = self.test_user.mfa_login_token
  186. request_mock.params = {'mfa_login_token': test_token}
  187. val_mock.return_value = True
  188. gen_mock.reset_mock()
  189. self.test_controller.mfa_login_post()
  190. gen_mock.assert_called_once_with(60 * 24 * 30)
  191. @mock.patch(ENVIRONMENT_PATH)
  192. @mock.patch(VALIDATE_PATH)
  193. def test_mfa_login_post_session(self, val_mock, env_mock, request_mock):
  194. '''Should log user in with new token as password if info valid'''
  195. request_mock.env = self.env
  196. request_mock.db = self.registry.db_name
  197. old_test_token = self.test_user.mfa_login_token
  198. request_mock.params = {'mfa_login_token': old_test_token}
  199. val_mock.return_value = True
  200. env_mock.return_value = self.env
  201. request_mock.reset_mock()
  202. self.test_controller.mfa_login_post()
  203. new_test_token = self.test_user.mfa_login_token
  204. request_mock.session.authenticate.assert_called_once_with(
  205. request_mock.db,
  206. self.test_user.login,
  207. new_test_token,
  208. self.test_user.id,
  209. )
  210. @mock.patch(GENERATE_PATH)
  211. @mock.patch(VALIDATE_PATH)
  212. def test_mfa_login_post_redirect(self, val_mock, gen_mock, request_mock):
  213. '''Should return correct redirect if info valid and redirect present'''
  214. request_mock.env = self.env
  215. request_mock.db = self.registry.db_name
  216. test_redir = 'Test Redir'
  217. request_mock.params = {
  218. 'mfa_login_token': self.test_user.mfa_login_token,
  219. 'redirect': test_redir,
  220. }
  221. val_mock.return_value = True
  222. test_result = self.test_controller.mfa_login_post()
  223. self.assertIn("window.location = '%s'" % test_redir, test_result.data)
  224. @mock.patch(GENERATE_PATH)
  225. @mock.patch(VALIDATE_PATH)
  226. def test_mfa_login_post_redir_def(self, val_mock, gen_mock, request_mock):
  227. '''Should return redirect to /web if info valid and no redirect'''
  228. request_mock.env = self.env
  229. request_mock.db = self.registry.db_name
  230. test_token = self.test_user.mfa_login_token
  231. request_mock.params = {'mfa_login_token': test_token}
  232. val_mock.return_value = True
  233. test_result = self.test_controller.mfa_login_post()
  234. self.assertIn("window.location = '/web'", test_result.data)
  235. @mock.patch(GENERATE_PATH)
  236. @mock.patch(VALIDATE_PATH)
  237. def test_mfa_login_post_device(self, val_mock, gen_mock, request_mock):
  238. '''Should add trusted device to user if remember flag set'''
  239. request_mock.env = self.env
  240. request_mock.db = self.registry.db_name
  241. test_token = self.test_user.mfa_login_token
  242. request_mock.params = {
  243. 'mfa_login_token': test_token,
  244. 'remember_device': True,
  245. }
  246. val_mock.return_value = True
  247. self.test_controller.mfa_login_post()
  248. self.assertEqual(len(self.test_user.trusted_device_ids), 1)
  249. @mock.patch(RESPONSE_PATH)
  250. @mock.patch(JSON_PATH)
  251. @mock.patch(GENERATE_PATH)
  252. @mock.patch(VALIDATE_PATH)
  253. def test_mfa_login_post_cookie_werkzeug_cookie(
  254. self, val_mock, gen_mock, json_mock, resp_mock, request_mock
  255. ):
  256. '''Should create Werkzeug cookie w/right info if remember flag set'''
  257. request_mock.env = self.env
  258. request_mock.db = self.registry.db_name
  259. test_token = self.test_user.mfa_login_token
  260. request_mock.params = {
  261. 'mfa_login_token': test_token,
  262. 'remember_device': True,
  263. }
  264. val_mock.return_value = True
  265. resp_mock().__class__ = Response
  266. json_mock.reset_mock()
  267. self.test_controller.mfa_login_post()
  268. test_device = self.test_user.trusted_device_ids
  269. config_model = self.env['ir.config_parameter']
  270. test_secret = config_model.get_param('database.secret')
  271. json_mock.assert_called_once_with(
  272. {'device_id': test_device.id},
  273. test_secret,
  274. )
  275. @mock.patch(DATETIME_PATH)
  276. @mock.patch(RESPONSE_PATH)
  277. @mock.patch(JSON_PATH)
  278. @mock.patch(GENERATE_PATH)
  279. @mock.patch(VALIDATE_PATH)
  280. def test_mfa_login_post_cookie_werkzeug_cookie_exp(
  281. self, val_mock, gen_mock, json_mock, resp_mock, dt_mock, request_mock
  282. ):
  283. '''Should serialize Werkzeug cookie w/right exp if remember flag set'''
  284. request_mock.env = self.env
  285. request_mock.db = self.registry.db_name
  286. test_token = self.test_user.mfa_login_token
  287. request_mock.params = {
  288. 'mfa_login_token': test_token,
  289. 'remember_device': True,
  290. }
  291. val_mock.return_value = True
  292. dt_mock.utcnow.return_value = datetime(2016, 12, 1)
  293. resp_mock().__class__ = Response
  294. json_mock.reset_mock()
  295. self.test_controller.mfa_login_post()
  296. json_mock().serialize.assert_called_once_with(datetime(2016, 12, 31))
  297. @mock.patch(DATETIME_PATH)
  298. @mock.patch(RESPONSE_PATH)
  299. @mock.patch(JSON_PATH)
  300. @mock.patch(GENERATE_PATH)
  301. @mock.patch(VALIDATE_PATH)
  302. def test_mfa_login_post_cookie_final_cookie(
  303. self, val_mock, gen_mock, json_mock, resp_mock, dt_mock, request_mock
  304. ):
  305. '''Should add correct cookie to response if remember flag set'''
  306. request_mock.env = self.env
  307. request_mock.db = self.registry.db_name
  308. test_token = self.test_user.mfa_login_token
  309. request_mock.params = {
  310. 'mfa_login_token': test_token,
  311. 'remember_device': True,
  312. }
  313. val_mock.return_value = True
  314. dt_mock.utcnow.return_value = datetime(2016, 12, 1)
  315. config_model = self.env['ir.config_parameter']
  316. config_model.set_param('auth_totp.secure_cookie', '0')
  317. resp_mock().__class__ = Response
  318. resp_mock.reset_mock()
  319. self.test_controller.mfa_login_post()
  320. resp_mock().set_cookie.assert_called_once_with(
  321. 'trusted_devices_%s' % self.test_user.id,
  322. json_mock().serialize(),
  323. max_age=30 * 24 * 60 * 60,
  324. expires=datetime(2016, 12, 31),
  325. httponly=True,
  326. secure=False,
  327. )
  328. @mock.patch(RESPONSE_PATH)
  329. @mock.patch(GENERATE_PATH)
  330. @mock.patch(VALIDATE_PATH)
  331. def test_mfa_login_post_cookie_final_cookie_secure(
  332. self, val_mock, gen_mock, resp_mock, request_mock
  333. ):
  334. '''Should set secure cookie if config parameter set accordingly'''
  335. request_mock.env = self.env
  336. request_mock.db = self.registry.db_name
  337. test_token = self.test_user.mfa_login_token
  338. request_mock.params = {
  339. 'mfa_login_token': test_token,
  340. 'remember_device': True,
  341. }
  342. val_mock.return_value = True
  343. config_model = self.env['ir.config_parameter']
  344. config_model.set_param('auth_totp.secure_cookie', '1')
  345. resp_mock().__class__ = Response
  346. resp_mock.reset_mock()
  347. self.test_controller.mfa_login_post()
  348. new_test_security = resp_mock().set_cookie.mock_calls[0][2]['secure']
  349. self.assertIs(new_test_security, True)
  350. @mock.patch(REDIRECT_PATH)
  351. @mock.patch(GENERATE_PATH)
  352. @mock.patch(VALIDATE_PATH)
  353. def test_mfa_login_post_firefox_response_returned(
  354. self, val_mock, gen_mock, redirect_mock, request_mock
  355. ):
  356. '''Should behave well if redirect returns Response (Firefox case)'''
  357. request_mock.env = self.env
  358. request_mock.db = self.registry.db_name
  359. redirect_mock.return_value = Response('Test Response')
  360. test_token = self.test_user.mfa_login_token
  361. request_mock.params = {'mfa_login_token': test_token}
  362. val_mock.return_value = True
  363. test_result = self.test_controller.mfa_login_post()
  364. self.assertIn('Test Response', test_result.response)