details:   https://code.tryton.org/tryton/commit/0e84ed4f4db8
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Jan 21 14:55:49 2026 +0100
description:
        Add support for basic authentication for user application

        Closes #14528
diffstat:

 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(-)

diffs (153 lines):

diff -r 7fd1ad0f0509 -r 0e84ed4f4db8 trytond/CHANGELOG
--- a/trytond/CHANGELOG Thu Jan 22 18:32:26 2026 +0100
+++ b/trytond/CHANGELOG Wed Jan 21 14:55:49 2026 +0100
@@ -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 -r 7fd1ad0f0509 -r 0e84ed4f4db8 trytond/doc/topics/user_application.rst
--- a/trytond/doc/topics/user_application.rst   Thu Jan 22 18:32:26 2026 +0100
+++ b/trytond/doc/topics/user_application.rst   Wed Jan 21 14:55:49 2026 +0100
@@ -52,8 +52,9 @@
 
 ``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 -r 7fd1ad0f0509 -r 0e84ed4f4db8 trytond/trytond/protocols/wrappers.py
--- a/trytond/trytond/protocols/wrappers.py     Thu Jan 22 18:32:26 2026 +0100
+++ b/trytond/trytond/protocols/wrappers.py     Wed Jan 21 14:55:49 2026 +0100
@@ -292,13 +292,17 @@
                 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 -r 7fd1ad0f0509 -r 0e84ed4f4db8 trytond/trytond/tests/test_protocols.py
--- a/trytond/trytond/tests/test_protocols.py   Thu Jan 22 18:32:26 2026 +0100
+++ b/trytond/trytond/tests/test_protocols.py   Wed Jan 21 14:55:49 2026 +0100
@@ -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 @@
         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('/<database_name>/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('/<database_name>/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('/<database_name>/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)

Reply via email to