From 712dddad39e9dba15476b29c3433f5e7b6d00c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Todorovich?= Date: Wed, 20 May 2026 16:33:16 -0300 Subject: [PATCH 1/3] [IMP] auth_api_key: add tests for controllers --- auth_api_key/tests/__init__.py | 1 + auth_api_key/tests/test_controllers.py | 47 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 auth_api_key/tests/test_controllers.py diff --git a/auth_api_key/tests/__init__.py b/auth_api_key/tests/__init__.py index 56e3e32a3a..308b13b596 100644 --- a/auth_api_key/tests/__init__.py +++ b/auth_api_key/tests/__init__.py @@ -1 +1,2 @@ from . import test_auth_api_key +from . import test_controllers diff --git a/auth_api_key/tests/test_controllers.py b/auth_api_key/tests/test_controllers.py new file mode 100644 index 0000000000..99cecf1d8e --- /dev/null +++ b/auth_api_key/tests/test_controllers.py @@ -0,0 +1,47 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from odoo.http import Controller, route +from odoo.tests import HttpCase, new_test_user +from odoo.tools import mute_logger + + +class TestControllers(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.AuthApiKey = cls.env["auth.api.key"] + cls.test_user = new_test_user( + cls.env, + name="Test User", + login="test", + password="test", + email="test@test.com", + group_ids=[cls.env.ref("base.group_user").id], + company_id=cls.env.company.id, + ) + cls.api_key = cls.AuthApiKey.create( + {"name": "good", "user_id": cls.test_user.id, "key": "api_key"} + ) + + class DummyController(Controller): + @route("/web/auth-api-key", type="http", auth="api_key", sitemap=False) + def auth_api_key(self, **params): + return json.dumps({"name": self.env.user.name}) + + cls.env.registry.clear_cache("routing") + cls.addClassCleanup(cls.env.registry.clear_cache, "routing") + + def test_auth_api_key_ok(self): + res = self.url_open("/web/auth-api-key", headers={"API-KEY": self.api_key.key}) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.json(), {"name": self.test_user.name}) + + @mute_logger("odoo.addons.base.models.ir_http") + def test_auth_api_key_wrong(self): + with self.assertLogs("odoo.http") as cm: + res = self.url_open("/web/auth-api-key", headers={"API-KEY": "wrong"}) + self.assertEqual(res.status_code, 403) + self.assertIn("Access Denied", cm.output[0]) From 9ed52968c1f50115b076178b624c78ab5ff325e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Todorovich?= Date: Wed, 20 May 2026 16:54:27 -0300 Subject: [PATCH 2/3] [FIX] auth_api_key: report allowed header in cors OPTIONS --- auth_api_key/__init__.py | 1 + auth_api_key/__manifest__.py | 1 + auth_api_key/hooks.py | 52 ++++++++++++++++++++++++++ auth_api_key/tests/test_controllers.py | 10 +++++ 4 files changed, 64 insertions(+) create mode 100644 auth_api_key/hooks.py diff --git a/auth_api_key/__init__.py b/auth_api_key/__init__.py index 0650744f6b..5bf67492bc 100644 --- a/auth_api_key/__init__.py +++ b/auth_api_key/__init__.py @@ -1 +1,2 @@ from . import models +from .hooks import post_load_hook diff --git a/auth_api_key/__manifest__.py b/auth_api_key/__manifest__.py index ce16e39fb5..8cdfac8ae5 100644 --- a/auth_api_key/__manifest__.py +++ b/auth_api_key/__manifest__.py @@ -11,6 +11,7 @@ "website": "https://github.com/OCA/server-auth", "development_status": "Production/Stable", "depends": ["base_setup"], + "post_load": "post_load_hook", "data": [ "security/ir.model.access.csv", "views/auth_api_key.xml", diff --git a/auth_api_key/hooks.py b/auth_api_key/hooks.py new file mode 100644 index 0000000000..6fb64d994d --- /dev/null +++ b/auth_api_key/hooks.py @@ -0,0 +1,52 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from werkzeug.exceptions import HTTPException +from werkzeug.wrappers import Response + +from odoo.http import Dispatcher + + +def patch_dispatcher_pre_dispatch(): + """Patch odoo.http.Dispatcher's pre_dispatch method. + + For routes with cors enabled, Odoo will add a handler for the OPTIONS method, + which will raise an abort HTTPException with a 204 response describing the allowed + methods and headers in the Access-Control-* respnose headers. + + For routes with api_key authentication, we must inform the client that the + API-KEY header is allowed. + """ + original_pre_dispatch = Dispatcher.pre_dispatch + + def pre_dispatch(self, rule, args): + get_header = self.request.future_response.headers.get + set_header = self.request.future_response.headers.set + routing = rule.endpoint.routing + if ( + routing.get("cors") + and routing.get("auth") == "api_key" + and self.request.httprequest.method == "OPTIONS" + ): + try: + return original_pre_dispatch(self, rule, args) + except HTTPException as e: + if ( + isinstance(e.response, Response) + and e.response.status_code == 204 + and get_header("Access-Control-Allow-Headers") + ): + set_header( + "Access-Control-Allow-Headers", + f"{get_header('Access-Control-Allow-Headers')}, API-Key", + ) + raise + else: + return original_pre_dispatch(self, rule, args) + + Dispatcher.pre_dispatch = pre_dispatch + Dispatcher.pre_dispatch._original_method = original_pre_dispatch + + +def post_load_hook(): + patch_dispatcher_pre_dispatch() diff --git a/auth_api_key/tests/test_controllers.py b/auth_api_key/tests/test_controllers.py index 99cecf1d8e..b965d1b49e 100644 --- a/auth_api_key/tests/test_controllers.py +++ b/auth_api_key/tests/test_controllers.py @@ -31,6 +31,10 @@ class DummyController(Controller): def auth_api_key(self, **params): return json.dumps({"name": self.env.user.name}) + @route("/web/auth-api-key-cors", type="http", auth="api_key", cors="*") + def auth_api_key_cors(self, **params): + return json.dumps({"name": self.env.user.name}) + cls.env.registry.clear_cache("routing") cls.addClassCleanup(cls.env.registry.clear_cache, "routing") @@ -45,3 +49,9 @@ def test_auth_api_key_wrong(self): res = self.url_open("/web/auth-api-key", headers={"API-KEY": "wrong"}) self.assertEqual(res.status_code, 403) self.assertIn("Access Denied", cm.output[0]) + + def test_auth_api_key_cors_options(self): + res = self.url_open("/web/auth-api-key-cors", method="OPTIONS") + self.assertEqual(res.status_code, 204) + self.assertEqual(res.headers["Access-Control-Allow-Origin"], "*") + self.assertIn("API-Key", res.headers["Access-Control-Allow-Headers"]) From 957944cccf3e0c78652e66516f79e5ff8ff2e79a Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 26 May 2026 13:40:09 +0000 Subject: [PATCH 3/3] [BOT] post-merge updates --- README.md | 2 +- auth_api_key/README.rst | 2 +- auth_api_key/__manifest__.py | 2 +- auth_api_key/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cbf4778cee..ac60de029e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[auth_api_key](auth_api_key/) | 19.0.1.0.1 | | Authenticate http requests from an API key +[auth_api_key](auth_api_key/) | 19.0.1.0.2 | | Authenticate http requests from an API key [auth_api_key_group](auth_api_key_group/) | 19.0.1.0.0 | simahawk | Allow grouping API keys together. Grouping per se does nothing. This feature is supposed to be used by other modules to limit access to services or records based on groups of keys. [auth_oauth_multi_token](auth_oauth_multi_token/) | 19.0.1.0.0 | | Allow multiple connection with the same OAuth account [auth_oidc](auth_oidc/) | 19.0.1.0.0 | sbidoul | Allow users to login through OpenID Connect Provider diff --git a/auth_api_key/README.rst b/auth_api_key/README.rst index d7a48e4d21..a9a06edb95 100644 --- a/auth_api_key/README.rst +++ b/auth_api_key/README.rst @@ -11,7 +11,7 @@ Auth Api Key !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:66a1fbebbd39def7609edaa4fea590c1828066ce3586ad30ba1c122ca3a106ec + !! source digest: sha256:0f3e857bf22cd42db4da93a05993023a71d0839e378d459b33ecd6063b9f6f51 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png diff --git a/auth_api_key/__manifest__.py b/auth_api_key/__manifest__.py index 8cdfac8ae5..afef0adc4b 100644 --- a/auth_api_key/__manifest__.py +++ b/auth_api_key/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth Api Key", "summary": """ Authenticate http requests from an API key""", - "version": "19.0.1.0.1", + "version": "19.0.1.0.2", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/server-auth", diff --git a/auth_api_key/static/description/index.html b/auth_api_key/static/description/index.html index 658f628557..9c70fd4298 100644 --- a/auth_api_key/static/description/index.html +++ b/auth_api_key/static/description/index.html @@ -372,7 +372,7 @@

Auth Api Key

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:66a1fbebbd39def7609edaa4fea590c1828066ce3586ad30ba1c122ca3a106ec +!! source digest: sha256:0f3e857bf22cd42db4da93a05993023a71d0839e378d459b33ecd6063b9f6f51 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: LGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

Authenticate http requests from an API key.