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.

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