From 5502313853e9315c93bf4b439a0fd628cac0f660 Mon Sep 17 00:00:00 2001 From: Julien Castiaux Date: Mon, 28 Nov 2022 10:36:04 +0000 Subject: [PATCH] [FIX] web, *: multi-db /web/session/authenticate *: base_setup, hr_timesheet, mail, partner_autocomplete, web_tour Start odoo without -d and with a --dbfilter that allows multiple databases. Via JSON-RPC access the /web/session/authenticate route providing a non-filtered database and valid credentials. Traceback, `request.env` is None. Since httpocalypse the initialization of the ORM (cursor, registry, environment) is greedy. It means that the connection to the database is established very early during the request routing or skip altogether in case no dbname was known at that time. This contrast with prepocalypse where the various ORM thingies were lazily setup the first time they were accessed. This changement has an important implication regarding authentication. In prepocalypse, thanks to the lazy approache, a cursor/registry/env would be setup on the database you just login upon using the `request.env` for the first time. This was very nice in this regard but had other problems. Since httpocalypse such operation is no more possible. Devs must initialize and use their own cursor/registry/env in case they authenticate on another database than the one `request.cr` is (maybe) connected to. The `/web/session/authenticate` controller is an example of such case. It crates its own cr/registry/environment after authentication. The problem the controller uses `ir.http.session_info` and that not all overrides were updated to use `self.env` (=the env created in the web controller) instead of `request.env` (=the missing env of the request). closes odoo/odoo#108063 X-original-commit: 7b9bd9d37731fae724dc5d91da656dab70aa9ad4 Related: odoo/enterprise#35012 Signed-off-by: Julien Castiaux --- addons/base_setup/models/ir_http.py | 5 ++-- addons/bus/models/ir_http.py | 4 +-- addons/hr_timesheet/models/ir_http.py | 7 ++--- addons/mail/models/ir_http.py | 4 +-- addons/partner_autocomplete/models/ir_http.py | 3 +- addons/web/controllers/session.py | 8 ++++++ addons/web/models/ir_http.py | 10 +++---- addons/web_tour/models/ir_http.py | 3 +- odoo/addons/test_http/tests/test_session.py | 28 +++++++++++++++++++ odoo/http.py | 1 + 10 files changed, 53 insertions(+), 20 deletions(-) diff --git a/addons/base_setup/models/ir_http.py b/addons/base_setup/models/ir_http.py index 4622d28f4f8..4e7bb7df171 100644 --- a/addons/base_setup/models/ir_http.py +++ b/addons/base_setup/models/ir_http.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models -from odoo.http import request class IrHttp(models.AbstractModel): @@ -9,6 +8,6 @@ class IrHttp(models.AbstractModel): def session_info(self): result = super(IrHttp, self).session_info() - if request.env.user._is_internal(): - result['show_effect'] = bool(request.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')) + if self.env.user._is_internal(): + result['show_effect'] = bool(self.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')) return result diff --git a/addons/bus/models/ir_http.py b/addons/bus/models/ir_http.py index 8c369326d88..7b6699f85ee 100644 --- a/addons/bus/models/ir_http.py +++ b/addons/bus/models/ir_http.py @@ -9,10 +9,10 @@ class IrHttp(models.AbstractModel): def session_info(self): result = super().session_info() - result['dbuuid'] = request.env['ir.config_parameter'].sudo().get_param('database.uuid') + result['dbuuid'] = self.env['ir.config_parameter'].sudo().get_param('database.uuid') return result def get_frontend_session_info(self): result = super().get_frontend_session_info() - result['dbuuid'] = request.env['ir.config_parameter'].sudo().get_param('database.uuid') + result['dbuuid'] = self.env['ir.config_parameter'].sudo().get_param('database.uuid') return result diff --git a/addons/hr_timesheet/models/ir_http.py b/addons/hr_timesheet/models/ir_http.py index eddb14e03a9..735e60be640 100644 --- a/addons/hr_timesheet/models/ir_http.py +++ b/addons/hr_timesheet/models/ir_http.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models -from odoo.http import request class Http(models.AbstractModel): @@ -13,8 +12,8 @@ class Http(models.AbstractModel): widget to apply, depending on the current company. """ result = super(Http, self).session_info() - if request.env.user._is_internal(): - company_ids = request.env.user.company_ids + if self.env.user._is_internal(): + company_ids = self.env.user.company_ids for company in company_ids: result["user_companies"]["allowed_companies"][company.id].update({ @@ -30,7 +29,7 @@ class Http(models.AbstractModel): @api.model def get_timesheet_uoms(self): - company_ids = request.env.user.company_ids + company_ids = self.env.user.company_ids uom_ids = company_ids.mapped('timesheet_encode_uom_id') | \ company_ids.mapped('project_time_mode_id') return { diff --git a/addons/mail/models/ir_http.py b/addons/mail/models/ir_http.py index ff89ab3a5fb..fe7c7e1d62c 100644 --- a/addons/mail/models/ir_http.py +++ b/addons/mail/models/ir_http.py @@ -9,7 +9,7 @@ class IrHttp(models.AbstractModel): _inherit = 'ir.http' def session_info(self): - user = request.env.user + user = self.env.user result = super(IrHttp, self).session_info() if self.env.user._is_internal(): result['notification_type'] = user.notification_type @@ -18,7 +18,7 @@ class IrHttp(models.AbstractModel): user_context = {'lang': guest.lang} mods = odoo.conf.server_wide_modules or [] lang = user_context.get("lang") - translation_hash = request.env['ir.http'].sudo().get_web_translations_hash(mods, lang) + translation_hash = self.env['ir.http'].sudo().get_web_translations_hash(mods, lang) result['cache_hashes']['translations'] = translation_hash result.update({ 'name': guest.name, diff --git a/addons/partner_autocomplete/models/ir_http.py b/addons/partner_autocomplete/models/ir_http.py index f927e24bad5..4f7ff87ee53 100644 --- a/addons/partner_autocomplete/models/ir_http.py +++ b/addons/partner_autocomplete/models/ir_http.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models -from odoo.http import request class Http(models.AbstractModel): @@ -12,5 +11,5 @@ class Http(models.AbstractModel): """ Add information about iap enrich to perform """ session_info = super(Http, self).session_info() if session_info.get('is_admin'): - session_info['iap_company_enrich'] = not request.env.user.company_id.iap_enrich_auto_done + session_info['iap_company_enrich'] = not self.env.user.company_id.iap_enrich_auto_done return session_info diff --git a/addons/web/controllers/session.py b/addons/web/controllers/session.py index 47e4f94cb2f..36eeab9bae5 100644 --- a/addons/web/controllers/session.py +++ b/addons/web/controllers/session.py @@ -40,6 +40,14 @@ class Session(http.Controller): registry = odoo.modules.registry.Registry(db) with registry.cursor() as cr: env = odoo.api.Environment(cr, request.session.uid, request.session.context) + if not request.db and not request.session.is_explicit: + # request._save_session would not update the session_token + # as it lacks an environment, rotating the session myself + http.root.session_store.rotate(request.session, env) + request.future_response.set_cookie( + 'session_id', request.session.sid, + max_age=http.SESSION_LIFETIME, httponly=True + ) return env['ir.http'].session_info() @http.route('/web/session/get_lang_list', type='json', auth="none") diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index d8f3fa4bf85..02287053691 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -65,7 +65,7 @@ class Http(models.AbstractModel): } def session_info(self): - user = request.env.user + user = self.env.user session_uid = request.session.uid version_info = odoo.service.common.exp_version() @@ -89,7 +89,7 @@ class Http(models.AbstractModel): "is_system": user._is_system() if session_uid else False, "is_admin": user._is_admin() if session_uid else False, "user_context": user_context, - "db": request.db, + "db": self.env.cr.dbname, "server_version": version_info.get('server_version'), "server_version_info": version_info.get('server_version_info'), "support_url": "https://www.odoo.com/buy", @@ -106,7 +106,7 @@ class Http(models.AbstractModel): "max_file_upload_size": max_file_upload_size, "home_action_id": user.action_id.id, "cache_hashes": { - "translations": request.env['ir.http'].sudo().get_web_translations_hash( + "translations": self.env['ir.http'].sudo().get_web_translations_hash( mods, request.session.context['lang'] ) if session_uid else None, }, @@ -122,7 +122,7 @@ class Http(models.AbstractModel): # but is still included in some other calls (e.g. '/web/session/authenticate') # to avoid access errors and unnecessary information, it is only included for users # with access to the backend ('internal'-type users) - menus = request.env['ir.ui.menu'].load_menus(request.session.debug) + menus = self.env['ir.ui.menu'].load_menus(request.session.debug) ordered_menus = {str(k): v for k, v in menus.items()} menu_json_utf8 = json.dumps(ordered_menus, default=ustr, sort_keys=True).encode() session_info['cache_hashes'].update({ @@ -174,6 +174,6 @@ class Http(models.AbstractModel): return session_info def get_currencies(self): - Currency = request.env['res.currency'] + Currency = self.env['res.currency'] currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places']) return {c['id']: {'symbol': c['symbol'], 'position': c['position'], 'digits': [69,c['decimal_places']]} for c in currencies} diff --git a/addons/web_tour/models/ir_http.py b/addons/web_tour/models/ir_http.py index bb44df13982..1d37ea1f834 100644 --- a/addons/web_tour/models/ir_http.py +++ b/addons/web_tour/models/ir_http.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models -from odoo.http import request class Http(models.AbstractModel): @@ -12,6 +11,6 @@ class Http(models.AbstractModel): result = super().session_info() if result['is_admin']: demo_modules_count = self.env['ir.module.module'].sudo().search_count([('demo', '=', True)]) - result['web_tours'] = request.env['web_tour.tour'].get_consumed_tours() + result['web_tours'] = self.env['web_tour.tour'].get_consumed_tours() result['tour_disable'] = demo_modules_count > 0 return result diff --git a/odoo/addons/test_http/tests/test_session.py b/odoo/addons/test_http/tests/test_session.py index 2737ca33dbd..c4163ae9a28 100644 --- a/odoo/addons/test_http/tests/test_session.py +++ b/odoo/addons/test_http/tests/test_session.py @@ -1,9 +1,11 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +import json from urllib.parse import urlparse from unittest.mock import patch import odoo +from odoo.tests.common import get_db_name from odoo.tools import mute_logger from .test_common import TestHttpBase @@ -91,3 +93,29 @@ class TestHttpSession(TestHttpBase): self.assertFalse(session['db']) self.assertEqual(res.status_code, 303) self.assertEqual(urlparse(res.headers['Location']).path, '/web/database/selector') + + def test_session4_web_authenticate_multidb(self): + self.db_list = [get_db_name(), 'another_database'] + + payload = json.dumps({ + 'jsonrpc': '2.0', + 'id': None, + 'method': 'call', + 'params': { + 'db': get_db_name(), + 'login': 'admin', + 'password': 'admin', + } + }) + + res = self.multidb_url_open( + '/web/session/authenticate', data=payload, headers={ + 'Content-Type': 'application/json', + } + ) + res.raise_for_status() + self.assertEqual(res.status_code, 200) + + res = self.multidb_url_open('/test_http/greeting-user') + res.raise_for_status() + self.assertEqual(res.status_code, 200, "Should not be redirected to /web/login") diff --git a/odoo/http.py b/odoo/http.py index 21424437713..e4922afed2a 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -857,6 +857,7 @@ class FilesystemSessionStore(sessions.FilesystemSessionStore): session.sid = self.generate_key() if session.uid and env: session.session_token = security.compute_session_token(session, env) + session.should_rotate = False self.save(session) def vacuum(self):