diff --git a/endpoint_auth_api_key/README.rst b/endpoint_auth_api_key/README.rst new file mode 100644 index 00000000..75848751 --- /dev/null +++ b/endpoint_auth_api_key/README.rst @@ -0,0 +1,102 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +===================== +Endpoint Auth API key +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cfd515c9f4adfa18204af3865093c9d6d00f92e79b06ce7d7e6ca353a4fc6934 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb--api-lightgray.png?logo=github + :target: https://github.com/OCA/web-api/tree/19.0/endpoint_auth_api_key + :alt: OCA/web-api +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-api-19-0/web-api-19-0-endpoint_auth_api_key + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web-api&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Provide API key auth for endpoints. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Simone Orsi + +- `Trobz `__: + + - Son Ho + +Other credits +------------- + + + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-simahawk| image:: https://github.com/simahawk.png?size=40px + :target: https://github.com/simahawk + :alt: simahawk + +Current `maintainer `__: + +|maintainer-simahawk| + +This module is part of the `OCA/web-api `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/endpoint_auth_api_key/__init__.py b/endpoint_auth_api_key/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/endpoint_auth_api_key/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/endpoint_auth_api_key/__manifest__.py b/endpoint_auth_api_key/__manifest__.py new file mode 100644 index 00000000..1dcc379c --- /dev/null +++ b/endpoint_auth_api_key/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2021 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Endpoint Auth API key", + "summary": """Provide API key auth for endpoints.""", + "version": "19.0.1.0.0", + "license": "LGPL-3", + "development_status": "Alpha", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["simahawk"], + "website": "https://github.com/OCA/web-api", + "depends": ["endpoint", "auth_api_key_group"], + "data": ["views/endpoint_view.xml"], +} diff --git a/endpoint_auth_api_key/i18n/endpoint_auth_api_key.pot b/endpoint_auth_api_key/i18n/endpoint_auth_api_key.pot new file mode 100644 index 00000000..9577f023 --- /dev/null +++ b/endpoint_auth_api_key/i18n/endpoint_auth_api_key.pot @@ -0,0 +1,30 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint_auth_api_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: endpoint_auth_api_key +#: model:ir.model.fields,field_description:endpoint_auth_api_key.field_endpoint_endpoint__auth_api_key_group_ids +#: model:ir.model.fields,field_description:endpoint_auth_api_key.field_endpoint_mixin__auth_api_key_group_ids +msgid "Allowed API key groups" +msgstr "" + +#. module: endpoint_auth_api_key +#: model:ir.model,name:endpoint_auth_api_key.model_endpoint_mixin +msgid "Endpoint mixin" +msgstr "" + +#. module: endpoint_auth_api_key +#: model_terms:ir.ui.view,arch_db:endpoint_auth_api_key.endpoint_mixin_form_view +msgid "Manage groups" +msgstr "" diff --git a/endpoint_auth_api_key/i18n/it.po b/endpoint_auth_api_key/i18n/it.po new file mode 100644 index 00000000..2a9fea0b --- /dev/null +++ b/endpoint_auth_api_key/i18n/it.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint_auth_api_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-04-02 12:35+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: endpoint_auth_api_key +#: model:ir.model.fields,field_description:endpoint_auth_api_key.field_endpoint_endpoint__auth_api_key_group_ids +#: model:ir.model.fields,field_description:endpoint_auth_api_key.field_endpoint_mixin__auth_api_key_group_ids +msgid "Allowed API key groups" +msgstr "Consenti gruppi chiavi API" + +#. module: endpoint_auth_api_key +#: model:ir.model,name:endpoint_auth_api_key.model_endpoint_mixin +msgid "Endpoint mixin" +msgstr "Mixin endpoint" + +#. module: endpoint_auth_api_key +#: model_terms:ir.ui.view,arch_db:endpoint_auth_api_key.endpoint_mixin_form_view +msgid "Manage groups" +msgstr "Gestione gruppi" diff --git a/endpoint_auth_api_key/i18n/zh_CN.po b/endpoint_auth_api_key/i18n/zh_CN.po new file mode 100644 index 00000000..a05ebe7d --- /dev/null +++ b/endpoint_auth_api_key/i18n/zh_CN.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint_auth_api_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-04-04 06:06+0000\n" +"Last-Translator: xtanuiha \n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 5.10.2\n" + +#. module: endpoint_auth_api_key +#: model:ir.model.fields,field_description:endpoint_auth_api_key.field_endpoint_endpoint__auth_api_key_group_ids +#: model:ir.model.fields,field_description:endpoint_auth_api_key.field_endpoint_mixin__auth_api_key_group_ids +msgid "Allowed API key groups" +msgstr "允许的API密钥组" + +#. module: endpoint_auth_api_key +#: model:ir.model,name:endpoint_auth_api_key.model_endpoint_mixin +msgid "Endpoint mixin" +msgstr "端点混入类" + +#. module: endpoint_auth_api_key +#: model_terms:ir.ui.view,arch_db:endpoint_auth_api_key.endpoint_mixin_form_view +msgid "Manage groups" +msgstr "管理组" diff --git a/endpoint_auth_api_key/models/__init__.py b/endpoint_auth_api_key/models/__init__.py new file mode 100644 index 00000000..95c62dfe --- /dev/null +++ b/endpoint_auth_api_key/models/__init__.py @@ -0,0 +1 @@ +from . import endpoint_mixin diff --git a/endpoint_auth_api_key/models/endpoint_mixin.py b/endpoint_auth_api_key/models/endpoint_mixin.py new file mode 100644 index 00000000..613d2079 --- /dev/null +++ b/endpoint_auth_api_key/models/endpoint_mixin.py @@ -0,0 +1,40 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import werkzeug + +from odoo import fields, models + + +class EndpointMixin(models.AbstractModel): + _inherit = "endpoint.mixin" + + auth_api_key_group_ids = fields.Many2many( + comodel_name="auth.api.key.group", + string="Allowed API key groups", + ) + + def _selection_auth_type(self): + return super()._selection_auth_type() + [("api_key", "API key")] + + def _validate_request(self, request): + super()._validate_request(request) + if self.auth_type == "api_key": + self._validate_api_key(request) + return + + def _validate_api_key(self, request): + key_id = request.auth_api_key_id + if key_id not in self._allowed_api_key_ids(): + self._logger.error("API key %s not allowed on %s", key_id, self.route) + raise werkzeug.exceptions.Forbidden() + + def _allowed_api_key_ids(self): + return self.auth_api_key_group_ids.auth_api_key_ids.ids + + def action_view_api_key_groups(self): + xmlid = "auth_api_key_group.auth_api_key_group_act_window" + action = self.env["ir.actions.act_window"]._for_xml_id(xmlid) + action["domain"] = [("id", "in", self.auth_api_key_group_ids.ids)] + return action diff --git a/endpoint_auth_api_key/pyproject.toml b/endpoint_auth_api_key/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/endpoint_auth_api_key/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/endpoint_auth_api_key/readme/CONTRIBUTORS.md b/endpoint_auth_api_key/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..f2fa2f75 --- /dev/null +++ b/endpoint_auth_api_key/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Simone Orsi \<\> + +- [Trobz](https://trobz.com): + + > - Son Ho \<\> diff --git a/endpoint_auth_api_key/readme/CREDITS.md b/endpoint_auth_api_key/readme/CREDITS.md new file mode 100644 index 00000000..e69de29b diff --git a/endpoint_auth_api_key/readme/DESCRIPTION.md b/endpoint_auth_api_key/readme/DESCRIPTION.md new file mode 100644 index 00000000..7d9743ba --- /dev/null +++ b/endpoint_auth_api_key/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Provide API key auth for endpoints. diff --git a/endpoint_auth_api_key/static/description/icon.png b/endpoint_auth_api_key/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/endpoint_auth_api_key/static/description/icon.png differ diff --git a/endpoint_auth_api_key/static/description/index.html b/endpoint_auth_api_key/static/description/index.html new file mode 100644 index 00000000..bcd52b84 --- /dev/null +++ b/endpoint_auth_api_key/static/description/index.html @@ -0,0 +1,449 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Endpoint Auth API key

+ +

Alpha License: LGPL-3 OCA/web-api Translate me on Weblate Try me on Runboat

+

Provide API key auth for endpoints.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

simahawk

+

This module is part of the OCA/web-api project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/endpoint_auth_api_key/tests/__init__.py b/endpoint_auth_api_key/tests/__init__.py new file mode 100644 index 00000000..6885a0f9 --- /dev/null +++ b/endpoint_auth_api_key/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_endpoint +from . import test_endpoint_controller diff --git a/endpoint_auth_api_key/tests/common.py b/endpoint_auth_api_key/tests/common.py new file mode 100644 index 00000000..f55fc6f7 --- /dev/null +++ b/endpoint_auth_api_key/tests/common.py @@ -0,0 +1,107 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import contextlib + +from odoo import Command +from odoo.tools import DotDict + +from odoo.addons.base.tests.common import TransactionCaseWithUserDemo +from odoo.addons.http_routing.tests.common import MockRequest + + +def _setup_demo_api_keys(env, demo_user): + """Create demo API keys for tests.""" + api_key_model = env["auth.api.key"] + + api_key_1 = api_key_model.create( + { + "name": "Endpoint API key demo", + "key": "cZ6dF2UQwNcm", + "user_id": demo_user.id, + } + ) + api_key_2 = api_key_model.create( + { + "name": "Endpoint API key demo 2", + "key": "kV47QyOTC5mS", + "user_id": demo_user.id, + } + ) + return api_key_1, api_key_2 + + +def _setup_demo_api_key_group(env, api_key_1): + """Create demo API key group for tests.""" + return env["auth.api.key.group"].create( + { + "name": "Demo Group 1", + "code": "demo_group1", + "auth_api_key_ids": [Command.set(api_key_1.ids)], + } + ) + + +def _setup_demo_endpoint(env, api_key_group): + """Create demo endpoint for tests.""" + return env["endpoint.endpoint"].create( + { + "name": "Demo Endpoint - auth api key", + "route": "/demo/api/key", + "request_method": "GET", + "auth_type": "api_key", + "auth_api_key_group_ids": [Command.set(api_key_group.ids)], + "exec_mode": "code", + "code_snippet": 'result = {"response": Response("ok")}', + } + ) + + +class CommonEndpointAuthAPIKey(TransactionCaseWithUserDemo): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_env() + cls._setup_records() + + @classmethod + def _setup_env(cls): + cls.env = cls.env(context=cls._setup_context()) + + @classmethod + def _setup_context(cls): + return dict( + cls.env.context, + tracking_disable=True, + ) + + @classmethod + def _setup_records(cls): + cls.api_key, cls.api_key2 = _setup_demo_api_keys( + cls.env, + cls.user_demo, + ) + cls.key_group = _setup_demo_api_key_group( + cls.env, + cls.api_key, + ) + cls.endpoint = _setup_demo_endpoint( + cls.env, + cls.key_group, + ) + + @contextlib.contextmanager + def _get_mocked_request( + self, httprequest=None, extra_headers=None, request_attrs=None + ): + with MockRequest(self.env) as mocked_request: + mocked_request.httprequest = ( + DotDict(httprequest) if httprequest else mocked_request.httprequest + ) + headers = {} + headers.update(extra_headers or {}) + mocked_request.httprequest.headers = headers + request_attrs = request_attrs or {} + for k, v in request_attrs.items(): + setattr(mocked_request, k, v) + mocked_request.make_response = lambda data, **kw: data + yield mocked_request diff --git a/endpoint_auth_api_key/tests/test_endpoint.py b/endpoint_auth_api_key/tests/test_endpoint.py new file mode 100644 index 00000000..3274fab6 --- /dev/null +++ b/endpoint_auth_api_key/tests/test_endpoint.py @@ -0,0 +1,58 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +import werkzeug + +from odoo.tools.misc import mute_logger + +from .common import CommonEndpointAuthAPIKey + + +class TestEndpoint(CommonEndpointAuthAPIKey): + @classmethod + def _setup_records(cls): + return super()._setup_records() + + @mute_logger("endpoint.endpoint") + def test_endpoint_validate_request_no_key(self): + endpoint = self.endpoint.copy( + { + "route": "/api-key-test", + "request_method": "GET", + } + ) + with self.assertRaises(werkzeug.exceptions.Forbidden): + with self._get_mocked_request( + httprequest={"method": "GET"}, + ) as req: + endpoint._validate_request(req) + + @mute_logger("endpoint.endpoint") + def test_endpoint_validate_request_bad_key(self): + endpoint = self.endpoint.copy( + { + "route": "/api-key-test", + "request_method": "GET", + } + ) + with self.assertRaises(werkzeug.exceptions.Forbidden): + with self._get_mocked_request( + httprequest={"method": "GET"}, + request_attrs={"auth_api_key_id": self.api_key2.id}, + ) as req: + endpoint._validate_request(req) + + def test_endpoint_validate_request_good_key(self): + endpoint = self.endpoint.copy( + { + "route": "/api-key-test", + "request_method": "GET", + } + ) + with self._get_mocked_request( + httprequest={"method": "GET"}, + request_attrs={"auth_api_key_id": self.api_key.id}, + ) as req: + endpoint._validate_request(req) diff --git a/endpoint_auth_api_key/tests/test_endpoint_controller.py b/endpoint_auth_api_key/tests/test_endpoint_controller.py new file mode 100644 index 00000000..a03abc13 --- /dev/null +++ b/endpoint_auth_api_key/tests/test_endpoint_controller.py @@ -0,0 +1,46 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import os +import unittest + +from odoo.tests.common import HttpCase +from odoo.tools.misc import mute_logger + +from .common import CommonEndpointAuthAPIKey + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointAuthApiKeyHttpCase skipped") +class EndpointAuthApiKeyHttpCase(HttpCase, CommonEndpointAuthAPIKey): + @classmethod + def setUpClass(cls): + super().setUpClass() + # force sync for demo records + cls.env["endpoint.endpoint"].search([])._handle_registry_sync() + + def tearDown(self): + # Clear cache for method ``ir.http.routing_map()`` + self.env.registry.clear_cache("routing") + super().tearDown() + + def _make_request(self, route, api_key=None, headers=None): + headers = dict(headers or {}) + if api_key: + headers["API-KEY"] = api_key.key + return self.url_open(route, headers=headers, timeout=60) + + @mute_logger("odoo.addons.auth_api_key.models.ir_http", "odoo.http") + def test_call_no_key(self): + response = self._make_request("/demo/api/key") + self.assertEqual(response.status_code, 401) + + def test_call_good_key(self): + response = self._make_request("/demo/api/key", api_key=self.api_key) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"ok") + + @mute_logger("endpoint.endpoint") + def test_call_bad_key(self): + response = self._make_request("/demo/api/key", api_key=self.api_key2) + self.assertEqual(response.status_code, 403) diff --git a/endpoint_auth_api_key/views/endpoint_view.xml b/endpoint_auth_api_key/views/endpoint_view.xml new file mode 100644 index 00000000..5b41970d --- /dev/null +++ b/endpoint_auth_api_key/views/endpoint_view.xml @@ -0,0 +1,25 @@ + + + + + endpoint.mixin + + + + +