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
277 lines
12 KiB
# -*- coding: utf-8 -*-
|
|
# Copyright 2016-2017 LasLabs Inc.
|
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
|
|
|
from mock import patch
|
|
from odoo.exceptions import AccessDenied, ValidationError
|
|
from odoo.tests.common import TransactionCase
|
|
from ..exceptions import MfaLoginNeeded
|
|
from ..models.res_users import JsonSecureCookie
|
|
from ..models.res_users_authenticator import ResUsersAuthenticator
|
|
|
|
MODEL_PATH = 'odoo.addons.auth_totp.models.res_users'
|
|
REQUEST_PATH = MODEL_PATH + '.request'
|
|
|
|
|
|
class TestResUsers(TransactionCase):
|
|
|
|
def setUp(self):
|
|
super(TestResUsers, self).setUp()
|
|
|
|
self.test_model = self.env['res.users']
|
|
|
|
self.test_user = self.env.ref('base.user_root')
|
|
self.test_user.mfa_enabled = False
|
|
self.test_user.authenticator_ids = False
|
|
self.env['res.users.authenticator'].create({
|
|
'name': 'Test Name',
|
|
'secret_key': 'Test Key',
|
|
'user_id': self.test_user.id,
|
|
})
|
|
self.test_user.mfa_enabled = True
|
|
|
|
self.env.uid = self.test_user.id
|
|
|
|
def test_compute_trusted_device_cookie_key_disable_mfa(self):
|
|
"""It should clear out existing key when MFA is disabled"""
|
|
self.test_user.mfa_enabled = False
|
|
|
|
self.assertFalse(self.test_user.trusted_device_cookie_key)
|
|
|
|
def test_compute_trusted_device_cookie_key_enable_mfa(self):
|
|
"""It should generate a new key when MFA is enabled"""
|
|
old_key = self.test_user.trusted_device_cookie_key
|
|
self.test_user.mfa_enabled = False
|
|
self.test_user.mfa_enabled = True
|
|
|
|
self.assertNotEqual(self.test_user.trusted_device_cookie_key, old_key)
|
|
|
|
def test_build_model_mfa_fields_in_self_writeable_list(self):
|
|
'''Should add MFA fields to list of fields users can modify for self'''
|
|
ResUsersClass = type(self.test_user)
|
|
self.assertIn('mfa_enabled', ResUsersClass.SELF_WRITEABLE_FIELDS)
|
|
self.assertIn('authenticator_ids', ResUsersClass.SELF_WRITEABLE_FIELDS)
|
|
|
|
def test_check_enabled_with_authenticator_mfa_no_auth(self):
|
|
'''Should raise correct error if MFA enabled without authenticators'''
|
|
with self.assertRaisesRegexp(ValidationError, 'locked out'):
|
|
self.test_user.authenticator_ids = False
|
|
|
|
def test_check_enabled_with_authenticator_no_mfa_auth(self):
|
|
'''Should not raise error if MFA not enabled with authenticators'''
|
|
try:
|
|
self.test_user.mfa_enabled = False
|
|
except ValidationError:
|
|
self.fail('A ValidationError was raised and should not have been.')
|
|
|
|
@patch(REQUEST_PATH, new=None)
|
|
def test_check_mfa_without_request(self):
|
|
"""It should remove UID from cache if in MFA cache and no request"""
|
|
test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
|
|
test_cache[self.env.uid] = 'test'
|
|
self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
|
|
try:
|
|
self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
|
|
except AccessDenied:
|
|
pass
|
|
|
|
self.assertFalse(test_cache.get(self.env.uid))
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_mfa_no_mfa_session(self, request_mock):
|
|
"""It should remove UID from cache if MFA cache but no MFA session"""
|
|
request_mock.session = {}
|
|
test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
|
|
test_cache[self.env.uid] = 'test'
|
|
self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
|
|
try:
|
|
self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
|
|
except AccessDenied:
|
|
pass
|
|
|
|
self.assertFalse(test_cache.get(self.env.uid))
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_mfa_invalid_mfa_session(self, request_mock):
|
|
"""It should remove UID if in MFA cache but invalid MFA session"""
|
|
request_mock.session = {'mfa_login_active': self.env.uid + 1}
|
|
test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
|
|
test_cache[self.env.uid] = 'test'
|
|
self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
|
|
try:
|
|
self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
|
|
except AccessDenied:
|
|
pass
|
|
|
|
self.assertFalse(test_cache.get(self.env.uid))
|
|
|
|
def test_check_no_mfa(self):
|
|
"""It should not remove UID from cache if not in MFA cache"""
|
|
test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
|
|
test_cache[self.env.uid] = 'test'
|
|
self.test_model._mfa_uid_cache[self.env.cr.dbname].clear()
|
|
self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
|
|
|
|
self.assertEqual(test_cache.get(self.env.uid), 'test')
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_mfa_valid_session(self, request_mock):
|
|
"""It should not remove UID if in MFA cache and valid session"""
|
|
request_mock.session = {'mfa_login_active': self.env.uid}
|
|
test_cache = self.test_model._Users__uid_cache[self.env.cr.dbname]
|
|
test_cache[self.env.uid] = 'test'
|
|
self.test_model._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
|
|
self.test_model.check(self.env.cr.dbname, self.env.uid, 'test')
|
|
|
|
self.assertEqual(test_cache.get(self.env.uid), 'test')
|
|
|
|
def test_check_credentials_mfa_not_enabled(self):
|
|
'''Should check password if user does not have MFA enabled'''
|
|
self.test_user.mfa_enabled = False
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
try:
|
|
self.env['res.users'].check_credentials('admin')
|
|
except AccessDenied:
|
|
self.fail('An exception was raised with a correct password.')
|
|
|
|
def test_check_credentials_mfa_uid_cache(self):
|
|
"""It should add user's ID to MFA UID cache if MFA enabled"""
|
|
self.test_model._mfa_uid_cache[self.env.cr.dbname].clear()
|
|
try:
|
|
self.test_model.check_credentials('invalid')
|
|
except AccessDenied:
|
|
pass
|
|
|
|
result_cache = self.test_model._mfa_uid_cache[self.env.cr.dbname]
|
|
self.assertEqual(result_cache, {self.test_user.id})
|
|
|
|
@patch(REQUEST_PATH, new=None)
|
|
def test_check_credentials_mfa_and_no_request(self):
|
|
'''Should raise correct exception if MFA enabled and no request'''
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
with self.assertRaises(MfaLoginNeeded):
|
|
self.env['res.users'].check_credentials('admin')
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_credentials_mfa_login_active(self, request_mock):
|
|
'''Should check password if user has finished MFA auth this session'''
|
|
request_mock.session = {'mfa_login_active': self.test_user.id}
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
try:
|
|
self.env['res.users'].check_credentials('admin')
|
|
except AccessDenied:
|
|
self.fail('An exception was raised with a correct password.')
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_credentials_mfa_different_login_active(self, request_mock):
|
|
'''Should correctly raise/update if other user finished MFA auth'''
|
|
request_mock.session = {'mfa_login_active': self.test_user.id + 1}
|
|
request_mock.httprequest.cookies = {}
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
self.assertFalse(request_mock.session.get('mfa_login_needed'))
|
|
with self.assertRaises(MfaLoginNeeded):
|
|
self.env['res.users'].check_credentials('admin')
|
|
self.assertTrue(request_mock.session.get('mfa_login_needed'))
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_credentials_mfa_no_device_cookie(self, request_mock):
|
|
'''Should correctly raise/update session if MFA and no device cookie'''
|
|
request_mock.session = {'mfa_login_active': False}
|
|
request_mock.httprequest.cookies = {}
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
self.assertFalse(request_mock.session.get('mfa_login_needed'))
|
|
with self.assertRaises(MfaLoginNeeded):
|
|
self.env['res.users'].check_credentials('admin')
|
|
self.assertTrue(request_mock.session.get('mfa_login_needed'))
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_credentials_mfa_corrupted_device_cookie(self, request_mock):
|
|
'''Should correctly raise/update session if MFA and corrupted cookie'''
|
|
request_mock.session = {'mfa_login_active': False}
|
|
test_key = 'trusted_devices_%d' % self.test_user.id
|
|
request_mock.httprequest.cookies = {test_key: 'invalid'}
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
self.assertFalse(request_mock.session.get('mfa_login_needed'))
|
|
with self.assertRaises(MfaLoginNeeded):
|
|
self.env['res.users'].check_credentials('admin')
|
|
self.assertTrue(request_mock.session.get('mfa_login_needed'))
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_credentials_mfa_cookie_from_wrong_user(self, request_mock):
|
|
'''Should raise and update session if MFA and wrong user's cookie'''
|
|
request_mock.session = {'mfa_login_active': False}
|
|
test_user_2 = self.env['res.users'].create({
|
|
'name': 'Test User',
|
|
'login': 'test_user',
|
|
})
|
|
test_id_2 = test_user_2.id
|
|
self.env['res.users.authenticator'].create({
|
|
'name': 'Test Name',
|
|
'secret_key': 'Test Key',
|
|
'user_id': test_id_2,
|
|
})
|
|
test_user_2.mfa_enabled = True
|
|
secret = test_user_2.trusted_device_cookie_key
|
|
test_device_cookie = JsonSecureCookie({'user_id': test_id_2}, secret)
|
|
test_device_cookie = test_device_cookie.serialize()
|
|
test_key = 'trusted_devices_%d' % self.test_user.id
|
|
request_mock.httprequest.cookies = {test_key: test_device_cookie}
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
self.assertFalse(request_mock.session.get('mfa_login_needed'))
|
|
with self.assertRaises(MfaLoginNeeded):
|
|
self.env['res.users'].check_credentials('admin')
|
|
self.assertTrue(request_mock.session.get('mfa_login_needed'))
|
|
|
|
@patch(REQUEST_PATH)
|
|
def test_check_credentials_mfa_correct_device_cookie(self, request_mock):
|
|
'''Should check password if MFA and correct device cookie'''
|
|
request_mock.session = {'mfa_login_active': False}
|
|
secret = self.test_user.trusted_device_cookie_key
|
|
test_device_cookie = JsonSecureCookie(
|
|
{'user_id': self.test_user.id},
|
|
secret,
|
|
)
|
|
test_device_cookie = test_device_cookie.serialize()
|
|
test_key = 'trusted_devices_%d' % self.test_user.id
|
|
request_mock.httprequest.cookies = {test_key: test_device_cookie}
|
|
|
|
with self.assertRaises(AccessDenied):
|
|
self.env['res.users'].check_credentials('invalid')
|
|
try:
|
|
self.env['res.users'].check_credentials('admin')
|
|
except AccessDenied:
|
|
self.fail('An exception was raised with a correct password.')
|
|
|
|
def test_validate_mfa_confirmation_code_not_singleton(self):
|
|
'''Should raise correct error when recordset is not singleton'''
|
|
test_user_2 = self.env['res.users']
|
|
test_user_3 = self.env.ref('base.public_user')
|
|
test_set = self.test_user + test_user_3
|
|
|
|
with self.assertRaisesRegexp(ValueError, 'Expected singleton'):
|
|
test_user_2.validate_mfa_confirmation_code('Test Code')
|
|
with self.assertRaisesRegexp(ValueError, 'Expected singleton'):
|
|
test_set.validate_mfa_confirmation_code('Test Code')
|
|
|
|
@patch.object(ResUsersAuthenticator, 'validate_conf_code')
|
|
def test_validate_mfa_confirmation_code_singleton_return(self, mock_func):
|
|
'''Should return validate_conf_code() value if singleton recordset'''
|
|
mock_func.return_value = 'Test Result'
|
|
|
|
self.assertEqual(
|
|
self.test_user.validate_mfa_confirmation_code('Test Code'),
|
|
'Test Result',
|
|
)
|