1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
|
import logging
import json
import time
from xmlrpc.client import Fault
from passlib.totp import TOTP
from odoo import http
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.tests import tagged, get_db_name
from odoo.tools import mute_logger
from ..controllers.home import Home
_logger = logging.getLogger(__name__)
class TestTOTPMixin:
def install_totphook(self):
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.registry.clear_cache('routing')
# 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.registry.clear_cache('routing')
@tagged('post_install', '-at_install')
class TestTOTP(HttpCaseWithUserDemo, TestTOTPMixin):
def setUp(self):
super().setUp()
self.install_totphook()
def test_totp(self):
# 1. Enable 2FA
self.start_tour('/odoo', '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.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.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('/odoo', 'totp_tour_setup', login='demo')
self.start_tour('/odoo', '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('/odoo', '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)
|