Install auth_oauth and via the /web/login, click the "Log in using Odoo.com" button. You are redirected on odoo.com which ask you for your odoo.com login and password. When the login form on odoo.com is submited, you are redirected back on your local database. The problem is that, in case a new account was created on-the-fly, then the login fails with a cryptic error. The actual error is that `request.env.user._is_internal` fails because `user` is an empty recordset where it should had been the just-authenticated user. The problem is an inconsistent transaction state between the cursor of the request, the cursor used with `auth_oauth` (which created a new user) and the cursor used with `authenticate` (which authenticated the new user). Yes, there are 3 cursors. The newly created user just isn't present in the transaction of the request's cursor. Here is the lifetime of the 3 cursors: * request.env.cr, it begins when the http request enters Odoo, it is commited when a http response exits Odoo. * /auth_oauth/signin, it begins roughly at the beginning of the controller, it is commited once after the user is created (so before the authenticate transaction begins but AFTER the request transaction begun), it is commited again when the controller exits. * authenticate, begins when authenticate is called, is commited when it returns. Because the request transaction started before, it cannot access user created by /auth_oauth/signin. Because the route is `auth='none'`, if system administrators append the `auth_oauth` module via `--load` (cli) or `server_wide_modules` (odoorc) then the controller can be accessed without database. This is the reason for the explicit registry/cursor/environment inside this controller, we needed to make sure we are connected to a database, we cannot rely on request. The new approach used in this work is to benefit from `ensure_db()`, the function that is used by various web `auth='none'` controllers such as /web and /web/login. It makes sure that the database we want to connect to is already present on the request, otherwise it repeats the request but this time connecting it to the database. Using this approach we can have a `auth='none'` controller whose request.env is guaranteed to be connected on the right database. We can avoid to create explicit new registry/cursor/environment within the controller and just use request's ones. Because the /auth_oauth/signin controller now simply use the request transaction, the above point: > Because the request transaction started before, it cannot access user > created by /auth_oauth/signin. just doesn't stand anymore as the user is created within the same transaction. The extra `cr.commit()` must still be present for `authenticate` to see the newly created user. opw-3421701 closes odoo/odoo#138051 X-original-commit: e165568f9795af213c8467a7f17948ed781b0799 Signed-off-by: Olivier Dony (odo) <odo@odoo.com> Co-authored-by: Julien Castiaux <juc@odoo.com>
219 lines
8.1 KiB
Python
219 lines
8.1 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import copy
|
|
import hashlib
|
|
import io
|
|
import logging
|
|
import re
|
|
from collections import OrderedDict, defaultdict
|
|
|
|
import babel.messages.pofile
|
|
import werkzeug
|
|
import werkzeug.exceptions
|
|
import werkzeug.utils
|
|
import werkzeug.wrappers
|
|
import werkzeug.wsgi
|
|
from lxml import etree
|
|
from werkzeug.urls import iri_to_uri
|
|
|
|
from odoo.tools.translate import JAVASCRIPT_TRANSLATION_COMMENT, WEB_TRANSLATION_COMMENT
|
|
from odoo.tools.misc import file_open
|
|
from odoo import http
|
|
from odoo.http import request
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def clean_action(action, env):
|
|
action_type = action.setdefault('type', 'ir.actions.act_window_close')
|
|
if action_type == 'ir.actions.act_window':
|
|
action = fix_view_modes(action)
|
|
|
|
# When returning an action, keep only relevant fields/properties
|
|
readable_fields = env[action['type']]._get_readable_fields()
|
|
action_type_fields = env[action['type']]._fields.keys()
|
|
|
|
cleaned_action = {
|
|
field: value
|
|
for field, value in action.items()
|
|
# keep allowed fields and custom properties fields
|
|
if field in readable_fields or field not in action_type_fields
|
|
}
|
|
|
|
# Warn about custom properties fields, because use is discouraged
|
|
action_name = action.get('name') or action
|
|
custom_properties = action.keys() - readable_fields - action_type_fields
|
|
if custom_properties:
|
|
_logger.warning("Action %r contains custom properties %s. Passing them "
|
|
"via the `params` or `context` properties is recommended instead",
|
|
action_name, ', '.join(map(repr, custom_properties)))
|
|
|
|
return cleaned_action
|
|
|
|
|
|
def ensure_db(redirect='/web/database/selector', db=None):
|
|
# This helper should be used in web client auth="none" routes
|
|
# if those routes needs a db to work with.
|
|
# If the heuristics does not find any database, then the users will be
|
|
# redirected to db selector or any url specified by `redirect` argument.
|
|
# If the db is taken out of a query parameter, it will be checked against
|
|
# `http.db_filter()` in order to ensure it's legit and thus avoid db
|
|
# forgering that could lead to xss attacks.
|
|
if db is None:
|
|
db = request.params.get('db') and request.params.get('db').strip()
|
|
|
|
# Ensure db is legit
|
|
if db and db not in http.db_filter([db]):
|
|
db = None
|
|
|
|
if db and not request.session.db:
|
|
# User asked a specific database on a new session.
|
|
# That mean the nodb router has been used to find the route
|
|
# Depending on installed module in the database, the rendering of the page
|
|
# may depend on data injected by the database route dispatcher.
|
|
# Thus, we redirect the user to the same page but with the session cookie set.
|
|
# This will force using the database route dispatcher...
|
|
r = request.httprequest
|
|
url_redirect = werkzeug.urls.url_parse(r.base_url)
|
|
if r.query_string:
|
|
# in P3, request.query_string is bytes, the rest is text, can't mix them
|
|
query_string = iri_to_uri(r.query_string)
|
|
url_redirect = url_redirect.replace(query=query_string)
|
|
request.session.db = db
|
|
werkzeug.exceptions.abort(request.redirect(url_redirect.to_url(), 302))
|
|
|
|
# if db not provided, use the session one
|
|
if not db and request.session.db and http.db_filter([request.session.db]):
|
|
db = request.session.db
|
|
|
|
# if no database provided and no database in session, use monodb
|
|
if not db:
|
|
all_dbs = http.db_list(force=True)
|
|
if len(all_dbs) == 1:
|
|
db = all_dbs[0]
|
|
|
|
# if no db can be found til here, send to the database selector
|
|
# the database selector will redirect to database manager if needed
|
|
if not db:
|
|
werkzeug.exceptions.abort(request.redirect(redirect, 303))
|
|
|
|
# always switch the session to the computed db
|
|
if db != request.session.db:
|
|
request.session = http.root.session_store.new()
|
|
request.session.update(http.get_default_session(), db=db)
|
|
request.session.context['lang'] = request.default_lang()
|
|
werkzeug.exceptions.abort(request.redirect(request.httprequest.url, 302))
|
|
|
|
|
|
def fix_view_modes(action):
|
|
""" For historical reasons, Odoo has weird dealings in relation to
|
|
view_mode and the view_type attribute (on window actions):
|
|
|
|
* one of the view modes is ``tree``, which stands for both list views
|
|
and tree views
|
|
* the choice is made by checking ``view_type``, which is either
|
|
``form`` for a list view or ``tree`` for an actual tree view
|
|
|
|
This methods simply folds the view_type into view_mode by adding a
|
|
new view mode ``list`` which is the result of the ``tree`` view_mode
|
|
in conjunction with the ``form`` view_type.
|
|
|
|
TODO: this should go into the doc, some kind of "peculiarities" section
|
|
|
|
:param dict action: an action descriptor
|
|
:returns: nothing, the action is modified in place
|
|
"""
|
|
if not action.get('views'):
|
|
generate_views(action)
|
|
|
|
if action.pop('view_type', 'form') != 'form':
|
|
return action
|
|
|
|
if 'view_mode' in action:
|
|
action['view_mode'] = ','.join(
|
|
mode if mode != 'tree' else 'list'
|
|
for mode in action['view_mode'].split(','))
|
|
action['views'] = [
|
|
[id, mode if mode != 'tree' else 'list']
|
|
for id, mode in action['views']
|
|
]
|
|
|
|
return action
|
|
|
|
|
|
# I think generate_views,fix_view_modes should go into js ActionManager
|
|
def generate_views(action):
|
|
"""
|
|
While the server generates a sequence called "views" computing dependencies
|
|
between a bunch of stuff for views coming directly from the database
|
|
(the ``ir.actions.act_window model``), it's also possible for e.g. buttons
|
|
to return custom view dictionaries generated on the fly.
|
|
|
|
In that case, there is no ``views`` key available on the action.
|
|
|
|
Since the web client relies on ``action['views']``, generate it here from
|
|
``view_mode`` and ``view_id``.
|
|
|
|
Currently handles two different cases:
|
|
|
|
* no view_id, multiple view_mode
|
|
* single view_id, single view_mode
|
|
|
|
:param dict action: action descriptor dictionary to generate a views key for
|
|
"""
|
|
view_id = action.get('view_id') or False
|
|
if isinstance(view_id, (list, tuple)):
|
|
view_id = view_id[0]
|
|
|
|
# providing at least one view mode is a requirement, not an option
|
|
view_modes = action['view_mode'].split(',')
|
|
|
|
if len(view_modes) > 1:
|
|
if view_id:
|
|
raise ValueError('Non-db action dictionaries should provide '
|
|
'either multiple view modes or a single view '
|
|
'mode and an optional view id.\n\n Got view '
|
|
'modes %r and view id %r for action %r' % (
|
|
view_modes, view_id, action))
|
|
action['views'] = [(False, mode) for mode in view_modes]
|
|
return
|
|
action['views'] = [(view_id, view_modes[0])]
|
|
|
|
|
|
def _get_login_redirect_url(uid, redirect=None):
|
|
""" Decide if user requires a specific post-login redirect, e.g. for 2FA, or if they are
|
|
fully logged and can proceed to the requested URL
|
|
"""
|
|
if request.session.uid: # fully logged
|
|
return redirect or ('/web' if is_user_internal(request.session.uid)
|
|
else '/web/login_successful')
|
|
|
|
# partial session (MFA)
|
|
url = request.env(user=uid)['res.users'].browse(uid)._mfa_url()
|
|
if not redirect:
|
|
return url
|
|
|
|
parsed = werkzeug.urls.url_parse(url)
|
|
qs = parsed.decode_query()
|
|
qs['redirect'] = redirect
|
|
return parsed.replace(query=werkzeug.urls.url_encode(qs)).to_url()
|
|
|
|
|
|
def is_user_internal(uid):
|
|
return request.env['res.users'].browse(uid)._is_internal()
|
|
|
|
|
|
def _local_web_translations(trans_file):
|
|
messages = []
|
|
try:
|
|
with file_open(trans_file, filter_ext=('.po')) as t_file:
|
|
po = babel.messages.pofile.read_po(t_file)
|
|
except Exception:
|
|
return
|
|
for x in po:
|
|
if x.id and x.string and (JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments
|
|
or WEB_TRANSLATION_COMMENT in x.auto_comments):
|
|
messages.append({'id': x.id, 'string': x.string})
|
|
return messages
|