From d7b80ff2ee9c9bf379c3fc4705689c8380aae741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 21 Jan 2026 14:55:49 +0100 Subject: [PATCH] Add support for basic authentication for user application Closes #14528 --- trytond/CHANGELOG | 1 + trytond/doc/topics/user_application.rst | 5 +- trytond/trytond/protocols/wrappers.py | 12 ++-- trytond/trytond/tests/test_protocols.py | 86 ++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index c77737ac1f..b70ddcd17e 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Add support for basic authentication for user application * Support the conversion of MJML reports to HTML * Allow filtering users to be notified by cron tasks diff --git a/trytond/doc/topics/user_application.rst b/trytond/doc/topics/user_application.rst index b669f5eaa7..6033518f17 100644 --- a/trytond/doc/topics/user_application.rst +++ b/trytond/doc/topics/user_application.rst @@ -52,8 +52,9 @@ creation of such route. ``user_application(name[, json])`` Set the :attr:`~trytond.transaction.Transaction.user` from the - ``Authorization`` header using the type ``bearer`` and a valid key for the - named user application. + ``Authorization`` header using the ``bearer`` type with the user application + key, or the ``basic`` type without a username and with the user application + key as the password. User Application Key ==================== diff --git a/trytond/trytond/protocols/wrappers.py b/trytond/trytond/protocols/wrappers.py index 90994b972d..83b4bccbc5 100644 --- a/trytond/trytond/protocols/wrappers.py +++ b/trytond/trytond/protocols/wrappers.py @@ -292,13 +292,17 @@ def user_application(name, json=True): authorization = parse_authorization_header(header) if authorization is None: abort(HTTPStatus.UNAUTHORIZED) - if authorization.type != 'bearer': - abort(HTTPStatus.FORBIDDEN) - token = getattr(authorization, 'token', '') + if authorization.type == 'bearer': + token = getattr(authorization, 'token', '') + elif authorization.type == 'basic': + token = authorization.get('password') + else: + abort(HTTPStatus.UNAUTHORIZED) + application = UserApplication.check(token, name) if not application: - abort(HTTPStatus.FORBIDDEN) + abort(HTTPStatus.UNAUTHORIZED) transaction = Transaction() # TODO language with transaction.set_user(application.user.id), \ diff --git a/trytond/trytond/tests/test_protocols.py b/trytond/trytond/tests/test_protocols.py index aaf1910998..36ec365e91 100644 --- a/trytond/trytond/tests/test_protocols.py +++ b/trytond/trytond/tests/test_protocols.py @@ -3,12 +3,17 @@ import datetime import json +from base64 import b64encode from decimal import Decimal +from trytond.pool import Pool from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder, JSONRequest +from trytond.protocols.wrappers import ( + HTTPStatus, Response, user_application, with_pool, with_transaction) from trytond.protocols.xmlrpc import XMLRequest, client -from trytond.tests.test_tryton import TestCase +from trytond.tests.test_tryton import Client, RouteTestCase, TestCase from trytond.tools.immutabledict import ImmutableDict +from trytond.wsgi import TrytondWSGI def _identity(x): @@ -130,3 +135,82 @@ class XMLTestCase(DumpsLoadsMixin, TestCase): result, _ = client.loads(s) result, = result self.assertEqual(result, Decimal('3.141592653589793')) + + +class UserApplication(RouteTestCase): + module = 'res' + + @classmethod + def setUpDatabase(cls): + pool = Pool() + User = pool.get('res.user') + UserApplication = pool.get('res.user.application') + + UserApplication.application.selection.append(('test', "Test")) + + User.create([{ + 'login': 'user', + 'applications': [('create', [{ + 'key': 'secret_key', + 'application': 'test', + 'state': 'validated', + }])], + }]) + + def test_authorization_bearer(self): + app = TrytondWSGI() + test_application = user_application('test') + + @app.route('//test/user_application') + @with_pool + @with_transaction() + @test_application + def _route(request, pool): + return '' + + client = Client(app, Response) + response = client.get( + f'/{self.db_name}/test/user_application', + headers=[ + ('Authorization', 'bearer secret_key'), + ]) + self.assertEqual(response.status_code, HTTPStatus.OK) + + def test_authorization_basic(self): + app = TrytondWSGI() + test_application = user_application('test') + + @app.route('//test/user_application') + @with_pool + @with_transaction() + @test_application + def _route(request, pool): + return '' + + client = Client(app, Response) + auth = b64encode(b':secret_key').decode('ascii') + response = client.get( + f'/{self.db_name}/test/user_application', + headers=[ + ('Authorization', f'basic {auth}'), + ]) + self.assertEqual(response.status_code, HTTPStatus.OK) + + def test_authorization_bad_auth(self): + app = TrytondWSGI() + test_application = user_application('test') + + @app.route('//test/user_application') + @with_pool + @with_transaction() + @test_application + def _route(request, pool): + return '' + + client = Client(app, Response) + response = client.get( + f'/{self.db_name}/test/user_application', + headers=[ + ('Authorization', 'bearer foo'), + ]) + self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)