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/__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..afef0adc4b 100644 --- a/auth_api_key/__manifest__.py +++ b/auth_api_key/__manifest__.py @@ -5,12 +5,13 @@ "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", "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/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.

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..b965d1b49e --- /dev/null +++ b/auth_api_key/tests/test_controllers.py @@ -0,0 +1,57 @@ +# 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}) + + @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") + + 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]) + + 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"])