Merge security.check_session and security.check

That way we won't use the pool when the database is not already initialized.

Closes #14316
This commit is contained in:
Nicolas Évrard
2025-10-24 18:16:51 +02:00
parent ecc3e152d1
commit 622457ba33
4 changed files with 178 additions and 63 deletions
+55 -47
View File
@@ -125,30 +125,69 @@ def reset_password(dbname, user, context=None):
def check(dbname, user, session, context=None):
for count in range(config.getint('database', 'retry'), -1, -1):
with Transaction().start(dbname, user, context=context) as transaction:
pool = _get_pool(dbname)
Session = pool.get('ir.session')
try:
find = Session.check(user, session)
break
except backend.DatabaseOperationalError:
if count:
continue
raise
finally:
transaction.commit()
remote_addr = _get_remote_addr(context)
database_list = Pool.database_list()
if dbname in database_list:
for count in range(config.getint('database', 'retry'), -1, -1):
with Transaction().start(dbname, user, context=context) \
as transaction:
pool = Pool(dbname)
Session = pool.get('ir.session')
try:
find = Session.check(user, session)
break
except backend.DatabaseOperationalError:
if count:
continue
raise
finally:
transaction.commit()
else:
if remote_addr:
ip_addr = str(ipaddress.ip_address(remote_addr))
else:
ip_addr = None
now = dt.datetime.now()
timeout = dt.timedelta(config.getint('session', 'max_age'))
database = backend.Database(dbname)
conn = database.get_connection(readonly=True)
try:
ir_session = Table('ir_session')
cursor = conn.cursor()
session_query = ir_session.select(
Coalesce(
ir_session.write_date, ir_session.create_date).as_('date'),
ir_session.key,
where=((ir_session.create_uid == user)
& (ir_session.ip_address == ip_addr)))
if backend.name == 'sqlite':
sqlite_apply_types(session_query, ['DATETIME', None])
cursor.execute(*session_query)
bad_session = False
for session_date, session_key in cursor:
if abs(session_date - now) < timeout:
if compare_digest(session_key, session):
find = session
break
else:
bad_session = True
else:
find = None if bad_session else ''
finally:
database.put_connection(conn)
if find is None:
logger.error("session failed for '%s' from '%s' on database '%s'",
user, _get_remote_addr(context), dbname)
user, remote_addr, dbname)
return
elif not find:
logger.info("session expired for '%s' from '%s' on database '%s'",
user, _get_remote_addr(context), dbname)
user, remote_addr, dbname)
return
else:
logger.debug("session valid for '%s' from '%s' on database '%s'",
user, _get_remote_addr(context), dbname)
user, remote_addr, dbname)
return user
@@ -172,37 +211,6 @@ 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()
session_query = ir_session.select(
Coalesce(
ir_session.write_date, ir_session.create_date).as_('date'),
ir_session.key,
where=((ir_session.create_uid == user)
& (ir_session.ip_address == ip_addr)))
if backend.name == 'sqlite':
sqlite_apply_types(session_query, ['DATETIME', None])
cursor.execute(*session_query)
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):
+27 -13
View File
@@ -29,30 +29,44 @@ class SecurityTestCase(RouteTestCase):
Transaction().commit()
@with_transaction()
def test_check_session(self):
"Testing check_session"
def _get_auth(self):
pool = Pool()
User = pool.get('res.user')
Session = pool.get('ir.session')
db_name = Transaction().database.name
user, = User.search([('login', '=', 'user')])
with Transaction().set_user(user.id):
key = Session.new()
Transaction().commit()
return user.id, key
user_id = security.check_session(db_name, user.id, key)
self.assertEqual(user_id, user.id)
def test_security_check(self):
"Test security.check"
user_id, key = self._get_auth()
authenticated_user_id = security.check(self.db_name, user_id, key)
self.assertEqual(authenticated_user_id, user_id)
@with_transaction()
def test_check_session_invalid(self):
"Testing check_session with an invalid session"
pool = Pool()
User = pool.get('res.user')
def test_security_check_invalid(self):
"Test security.check with an invalid session"
user_id, _ = self._get_auth()
user_id = security.check(self.db_name, user_id, "invalid key")
self.assertIsNone(user_id)
db_name = Transaction().database.name
user, = User.search([('login', '=', 'user')])
def test_security_check_no_pool(self):
"Test security.check without the pool"
user_id, key = self._get_auth()
Pool.stop(self.db_name)
user_id = security.check_session(db_name, user.id, "invalid key")
authenticated_user_id = security.check(self.db_name, user_id, key)
self.assertNotIn(self.db_name, Pool._pools)
self.assertEqual(authenticated_user_id, user_id)
def test_security_check_no_pool_invalid(self):
"Test security.check without the pool on an invalid session"
user_id, _ = self._get_auth()
Pool.stop(self.db_name)
user_id = security.check(self.db_name, user_id, "invalid key")
self.assertNotIn(self.db_name, Pool._pools)
self.assertIsNone(user_id)
+89 -1
View File
@@ -1,13 +1,17 @@
# 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 base64
from http import HTTPStatus
from unittest.mock import Mock, sentinel
from werkzeug.routing import Map, Rule
from trytond import security
from trytond.exceptions import TrytonException
from trytond.pool import Pool
from trytond.protocols.wrappers import Response
from trytond.tests.test_tryton import Client, TestCase
from trytond.tests.test_tryton import Client, RouteTestCase, TestCase
from trytond.wsgi import Base64Converter, TrytondWSGI
@@ -133,3 +137,87 @@ class WSGIAppTestCase(TestCase):
self.assertEqual(next(response.response), b'baz')
self.assertEqual(response.status, "418 I'M A TEAPOT")
class TrytonWSGITestCase(RouteTestCase):
module = 'res'
@classmethod
def setUpDatabase(cls):
pool = Pool()
User = pool.get('res.user')
User.create([{
'name': 'user',
'login': 'user',
'password': '12345678',
}])
def test_session_valid_good_auth(self):
"Test that session_valid correctly authenticates"
app = TrytondWSGI()
@app.route('/<database_name>/session_required')
@app.session_valid
def _route(request, database_name):
return Response(b'')
user_id, key = security.login(
self.db_name, 'user', {'password': '12345678'})
client = Client(app, Response)
session_hdr = 'Session ' + base64.b64encode(
f'user:{user_id}:{key}'.encode('utf8')).decode('utf8')
response = client.get(
f'/{self.db_name}/session_required',
headers=[('Authorization', session_hdr)])
self.assertEqual(response.status_code, HTTPStatus.OK)
def test_session_valid_no_pool(self):
"Test that session_valid does not use the pool"
app = TrytondWSGI()
@app.route('/<database_name>/session_required')
@app.session_valid
def _route(request):
return Response(b'')
user_id, key = security.login(
self.db_name, 'user', {'password': '12345678'})
Pool.stop(self.db_name)
client = Client(app, Response)
session_hdr = 'Session ' + base64.b64encode(
f'user:{user_id}:{key}'.encode('utf8')).decode('utf8')
client.get(
f'/{self.db_name}/session_required',
headers=[('Authorization', session_hdr)])
self.assertNotIn(self.db_name, Pool._pools)
def test_session_valid_bad_auth(self):
"Test that session_valid refuse wrong Authentication headers"
app = TrytondWSGI()
@app.route('/<database_name>/session_required')
@app.session_valid
def _route(request):
return Response(b'')
client = Client(app, Response)
response = client.get(
f'/{self.db_name}/session_required',
headers=[('Authorization', 'Session bad token')])
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
def test_session_valid_no_auth(self):
"Test that session_valid refuse unauthenticated requests"
app = TrytondWSGI()
@app.route('/<database_name>/session_required')
@app.session_valid
def _route(request):
return Response(b'')
client = Client(app, Response)
response = client.get(f'/{self.db_name}/session_required')
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
+7 -2
View File
@@ -102,8 +102,13 @@ class TrytondWSGI(object):
session = request.authorization.get('session')
dbname = request.view_args.get('database_name')
if not security.check_session(
dbname, userid, session, request.remote_addr):
session_check = security.check(
dbname, userid, session, {
'_request': {
'remote_addr': request.remote_addr,
},
})
if session_check is None:
_do_basic_auth(request)
return func(request, *args, **kwargs)