Skip to content

Commit 3ec966e

Browse files
committed
Merge PR #943 into 19.0
Signed-off-by simahawk
2 parents 21cb186 + 9ed5296 commit 3ec966e

5 files changed

Lines changed: 112 additions & 0 deletions

File tree

auth_api_key/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import models
2+
from .hooks import post_load_hook

auth_api_key/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"website": "https://github.com/OCA/server-auth",
1212
"development_status": "Production/Stable",
1313
"depends": ["base_setup"],
14+
"post_load": "post_load_hook",
1415
"data": [
1516
"security/ir.model.access.csv",
1617
"views/auth_api_key.xml",

auth_api_key/hooks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from werkzeug.exceptions import HTTPException
5+
from werkzeug.wrappers import Response
6+
7+
from odoo.http import Dispatcher
8+
9+
10+
def patch_dispatcher_pre_dispatch():
11+
"""Patch odoo.http.Dispatcher's pre_dispatch method.
12+
13+
For routes with cors enabled, Odoo will add a handler for the OPTIONS method,
14+
which will raise an abort HTTPException with a 204 response describing the allowed
15+
methods and headers in the Access-Control-* respnose headers.
16+
17+
For routes with api_key authentication, we must inform the client that the
18+
API-KEY header is allowed.
19+
"""
20+
original_pre_dispatch = Dispatcher.pre_dispatch
21+
22+
def pre_dispatch(self, rule, args):
23+
get_header = self.request.future_response.headers.get
24+
set_header = self.request.future_response.headers.set
25+
routing = rule.endpoint.routing
26+
if (
27+
routing.get("cors")
28+
and routing.get("auth") == "api_key"
29+
and self.request.httprequest.method == "OPTIONS"
30+
):
31+
try:
32+
return original_pre_dispatch(self, rule, args)
33+
except HTTPException as e:
34+
if (
35+
isinstance(e.response, Response)
36+
and e.response.status_code == 204
37+
and get_header("Access-Control-Allow-Headers")
38+
):
39+
set_header(
40+
"Access-Control-Allow-Headers",
41+
f"{get_header('Access-Control-Allow-Headers')}, API-Key",
42+
)
43+
raise
44+
else:
45+
return original_pre_dispatch(self, rule, args)
46+
47+
Dispatcher.pre_dispatch = pre_dispatch
48+
Dispatcher.pre_dispatch._original_method = original_pre_dispatch
49+
50+
51+
def post_load_hook():
52+
patch_dispatcher_pre_dispatch()

auth_api_key/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import test_auth_api_key
2+
from . import test_controllers
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
import json
5+
6+
from odoo.http import Controller, route
7+
from odoo.tests import HttpCase, new_test_user
8+
from odoo.tools import mute_logger
9+
10+
11+
class TestControllers(HttpCase):
12+
@classmethod
13+
def setUpClass(cls):
14+
super().setUpClass()
15+
cls.AuthApiKey = cls.env["auth.api.key"]
16+
cls.test_user = new_test_user(
17+
cls.env,
18+
name="Test User",
19+
login="test",
20+
password="test",
21+
email="test@test.com",
22+
group_ids=[cls.env.ref("base.group_user").id],
23+
company_id=cls.env.company.id,
24+
)
25+
cls.api_key = cls.AuthApiKey.create(
26+
{"name": "good", "user_id": cls.test_user.id, "key": "api_key"}
27+
)
28+
29+
class DummyController(Controller):
30+
@route("/web/auth-api-key", type="http", auth="api_key", sitemap=False)
31+
def auth_api_key(self, **params):
32+
return json.dumps({"name": self.env.user.name})
33+
34+
@route("/web/auth-api-key-cors", type="http", auth="api_key", cors="*")
35+
def auth_api_key_cors(self, **params):
36+
return json.dumps({"name": self.env.user.name})
37+
38+
cls.env.registry.clear_cache("routing")
39+
cls.addClassCleanup(cls.env.registry.clear_cache, "routing")
40+
41+
def test_auth_api_key_ok(self):
42+
res = self.url_open("/web/auth-api-key", headers={"API-KEY": self.api_key.key})
43+
self.assertEqual(res.status_code, 200)
44+
self.assertEqual(res.json(), {"name": self.test_user.name})
45+
46+
@mute_logger("odoo.addons.base.models.ir_http")
47+
def test_auth_api_key_wrong(self):
48+
with self.assertLogs("odoo.http") as cm:
49+
res = self.url_open("/web/auth-api-key", headers={"API-KEY": "wrong"})
50+
self.assertEqual(res.status_code, 403)
51+
self.assertIn("Access Denied", cm.output[0])
52+
53+
def test_auth_api_key_cors_options(self):
54+
res = self.url_open("/web/auth-api-key-cors", method="OPTIONS")
55+
self.assertEqual(res.status_code, 204)
56+
self.assertEqual(res.headers["Access-Control-Allow-Origin"], "*")
57+
self.assertIn("API-Key", res.headers["Access-Control-Allow-Headers"])

0 commit comments

Comments
 (0)