Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <a href='https://github.com/simahawk'><img src='https://github.com/simahawk.png' width='32' height='32' style='border-radius:50%;' alt='simahawk'/></a> | 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 | <a href='https://github.com/sbidoul'><img src='https://github.com/sbidoul.png' width='32' height='32' style='border-radius:50%;' alt='sbidoul'/></a> | Allow users to login through OpenID Connect Provider
Expand Down
2 changes: 1 addition & 1 deletion auth_api_key/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions auth_api_key/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from .hooks import post_load_hook
3 changes: 2 additions & 1 deletion auth_api_key/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 52 additions & 0 deletions auth_api_key/hooks.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion auth_api_key/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ <h1>Auth Api Key</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:66a1fbebbd39def7609edaa4fea590c1828066ce3586ad30ba1c122ca3a106ec
!! source digest: sha256:0f3e857bf22cd42db4da93a05993023a71d0839e378d459b33ecd6063b9f6f51
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-auth/tree/19.0/auth_api_key"><img alt="OCA/server-auth" src="https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-auth-19-0/server-auth-19-0-auth_api_key"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-auth&amp;target_branch=19.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Authenticate http requests from an API key.</p>
Expand Down
1 change: 1 addition & 0 deletions auth_api_key/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import test_auth_api_key
from . import test_controllers
57 changes: 57 additions & 0 deletions auth_api_key/tests/test_controllers.py
Original file line number Diff line number Diff line change
@@ -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"])
Loading