diff --git a/trytond/trytond/bus.py b/trytond/trytond/bus.py index b93f16970b..938a8d3804 100644 --- a/trytond/trytond/bus.py +++ b/trytond/trytond/bus.py @@ -226,7 +226,7 @@ else: @app.route('//bus', methods=['POST']) -@app.auth_required +@app.session_valid def subscribe(request, database_name): if not _allow_subscribe: raise exceptions.NotImplemented diff --git a/trytond/trytond/security.py b/trytond/trytond/security.py index 6fab3e35d8..6fe0eea9f0 100644 --- a/trytond/trytond/security.py +++ b/trytond/trytond/security.py @@ -1,15 +1,19 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime as dt +import ipaddress import logging import random import time +from secrets import compare_digest try: from http import HTTPStatus except ImportError: from http import client as HTTPStatus +from sql import Table +from sql.conditionals import Coalesce from werkzeug.exceptions import abort from trytond import backend @@ -168,6 +172,33 @@ def check_timeout(dbname, user, session, context=None): return valid +def check_session(dbname, user, session, remote_addr=None): + "Check the session without using the pool" + database = backend.Database(dbname) + now = dt.datetime.now() + timeout = dt.timedelta(config.getint('session', 'max_age')) + conn = database.get_connection(readonly=True) + if remote_addr: + ip_addr = str(ipaddress.ip_address(remote_addr)) + else: + ip_addr = None + try: + ir_session = Table('ir_session') + cursor = conn.cursor() + cursor.execute(*ir_session.select( + Coalesce(ir_session.write_date, ir_session.create_date), + ir_session.key, + where=((ir_session.create_uid == user) + & (ir_session.ip_address == ip_addr)))) + for session_date, session_key in cursor: + if abs(session_date - now) < timeout: + if compare_digest(session_key, session): + return True + return False + finally: + database.put_connection(conn) + + def reset(dbname, session, context): try: with Transaction().start(dbname, 0, context=context, autocommit=True): diff --git a/trytond/trytond/wsgi.py b/trytond/trytond/wsgi.py index 78bb547719..57ef856320 100644 --- a/trytond/trytond/wsgi.py +++ b/trytond/trytond/wsgi.py @@ -27,7 +27,7 @@ try: except ImportError: from werkzeug.wsgi import SharedDataMiddleware -from trytond import backend +from trytond import backend, security from trytond.config import config from trytond.protocols.jsonrpc import JSONProtocol from trytond.protocols.wrappers import ( @@ -41,6 +41,14 @@ __all__ = ['TrytondWSGI', 'app'] logger = logging.getLogger(__name__) +def _do_basic_auth(request): + headers = {} + if request.headers.get('X-Requested-With') != 'XMLHttpRequest': + headers['WWW-Authenticate'] = 'Basic realm="Tryton"' + response = Response(None, http.client.UNAUTHORIZED, headers) + abort(http.client.UNAUTHORIZED, response=response) + + class Base64Converter(BaseConverter): def to_python(self, value): @@ -81,11 +89,25 @@ class TrytondWSGI(object): if request.user_id: return func(request, *args, **kwargs) else: - headers = {} - if request.headers.get('X-Requested-With') != 'XMLHttpRequest': - headers['WWW-Authenticate'] = 'Basic realm="Tryton"' - response = Response(None, http.client.UNAUTHORIZED, headers) - abort(http.client.UNAUTHORIZED, response=response) + _do_basic_auth(request) + return wrapper + + def session_valid(self, func): + @wraps(func) + def wrapper(request, *args, **kwargs): + if (not request.authorization + or request.authorization.type != 'session'): + _do_basic_auth(request) + userid = request.authorization.get('userid') + session = request.authorization.get('session') + dbname = request.view_args.get('database_name') + + if not security.check_session( + dbname, userid, session, request.remote_addr): + _do_basic_auth(request) + + return func(request, *args, **kwargs) + return wrapper def dispatch_request(self, request):