Before this revision, when you pass `context` in the arguments of a JSON routes, this one gets automatically injected in the environment context. This is not the case for regular HTTP routes. It makes sense to propagate the context for the JSONRPC protocol, JSON routes used by the backend, such as `call_kw`, but it doesn't make sense to pass this context automatically for any other kind of routes, such as front-end routes or routes used by custom Javascript widgets. This change brings a more unified behavior for routes of types HTTP and JSON. In addition, most developers were not aware of this "feautre", that passing `context` in the arguments of a JSON route leaded to the injection of this context in the environment context. This is actually reflected by the diff size this changes required, only a dozens of routes needed to be adapted, to manually add the context in their route arguments and to inject it in their environment context. closes odoo/odoo#121726 X-original-commit: a7a5655631e6d5b05fd2ba3d0c80617aae6d9cfe Related: odoo/enterprise#41229 Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
import json
|
|
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 odoo.tools import mute_logger
|
|
|
|
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
|
|
self.start_tour('/', 'totp_login_enabled', login=None)
|
|
|
|
# 4. Check 2FA is not requested on saved device and disable it
|
|
self.start_tour('/', 'totp_login_device', login=None)
|
|
|
|
# 5. Finally, check that 2FA is in fact disabled
|
|
self.start_tour('/', 'totp_login_disabled', login=None)
|
|
|
|
# 6. 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)
|
|
|
|
@mute_logger('odoo.http')
|
|
def test_totp_authenticate(self):
|
|
"""
|
|
Ensure we don't leak the session info from an half-logged-in
|
|
user.
|
|
"""
|
|
|
|
self.start_tour('/web', 'totp_tour_setup', login='demo')
|
|
self.url_open('/web/session/logout')
|
|
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": "call",
|
|
"id": 0,
|
|
"params": {
|
|
"db": get_db_name(),
|
|
"login": "demo",
|
|
"password": "demo",
|
|
},
|
|
}
|
|
response = self.url_open("/web/session/authenticate", data=json.dumps(payload), headers=headers)
|
|
data = response.json()
|
|
self.assertEqual(data['result']['uid'], None)
|