* Not sure how the tour passed during merge as it would not be looking for the button in the right tab, it would fail locally, fix this issue by properly switching to the Account Security tab * add the missing test of RPC (which should not work on an account with totp enabled) * also reorder ops & fix comments: turns out `totp_login_enabled` checks that totp is enabled *then disables it* closes odoo/odoo#55979 Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
import time
|
|
from xmlrpc.client import Fault
|
|
|
|
from passlib.totp import TOTP
|
|
|
|
from odoo import http
|
|
from odoo.exceptions import AccessDenied
|
|
from odoo.service import common as auth, model
|
|
from odoo.tests import tagged, HttpCase, get_db_name
|
|
|
|
from ..controllers.home import Home
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestTOTP(HttpCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
totp = None
|
|
# might be possible to do client-side using `crypto.subtle` instead of
|
|
# this horror show, but requires working on 64b integers, & BigInt is
|
|
# significantly less well supported than crypto
|
|
def totp_hook(self, secret=None):
|
|
nonlocal totp
|
|
if totp is None:
|
|
totp = TOTP(secret)
|
|
if secret:
|
|
return totp.generate().token
|
|
else:
|
|
# on check, take advantage of window because previous token has been
|
|
# "burned" so we can't generate the same, but tour is so fast
|
|
# we're pretty certainly within the same 30s
|
|
return totp.generate(time.time() + 30).token
|
|
# because not preprocessed by ControllerType metaclass
|
|
totp_hook.routing_type = 'json'
|
|
self.env['ir.http']._clear_routing_map()
|
|
# patch Home to add test endpoint
|
|
Home.totp_hook = http.route('/totphook', type='json', auth='none')(totp_hook)
|
|
# remove endpoint and destroy routing map
|
|
@self.addCleanup
|
|
def _cleanup():
|
|
del Home.totp_hook
|
|
self.env['ir.http']._clear_routing_map()
|
|
|
|
def test_totp(self):
|
|
# 1. Enable 2FA
|
|
self.start_tour('/web', 'totp_tour_setup', login='demo')
|
|
|
|
# 2. Verify that RPC is blocked because 2FA is on.
|
|
self.assertFalse(
|
|
self.xmlrpc_common.authenticate(get_db_name(), 'demo', 'demo', {}),
|
|
"Should not have returned a uid"
|
|
)
|
|
self.assertFalse(
|
|
self.xmlrpc_common.authenticate(get_db_name(), 'demo', 'demo', {'interactive': True}),
|
|
'Trying to fake the auth type should not work'
|
|
)
|
|
uid = self.env.ref('base.user_demo').id
|
|
with self.assertRaisesRegex(Fault, r'Access Denied'):
|
|
self.xmlrpc_object.execute_kw(
|
|
get_db_name(), uid, 'demo',
|
|
'res.users', 'read', [uid, ['login']]
|
|
)
|
|
|
|
# 3. Check 2FA is required and disable it
|
|
self.start_tour('/', 'totp_login_enabled', login=None)
|
|
|
|
# 4. Finally, check that 2FA is in fact disabled
|
|
self.start_tour('/', 'totp_login_disabled', login=None)
|
|
|
|
# 5. Check that rpc is now re-allowed
|
|
uid = self.xmlrpc_common.authenticate(get_db_name(), 'demo', 'demo', {})
|
|
self.assertEqual(uid, self.env.ref('base.user_demo').id)
|
|
[r] = self.xmlrpc_object.execute_kw(
|
|
get_db_name(), uid, 'demo',
|
|
'res.users', 'read', [uid, ['login']]
|
|
)
|
|
self.assertEqual(r['login'], 'demo')
|
|
|
|
|
|
def test_totp_administration(self):
|
|
self.start_tour('/web', 'totp_tour_setup', login='demo')
|
|
self.start_tour('/web', 'totp_admin_disables', login='admin')
|
|
self.start_tour('/', 'totp_login_disabled', login=None)
|