From caaa7fcb1d465c61a3f8c7fc2030f6b2708f1f02 Mon Sep 17 00:00:00 2001 From: Mmequignon Date: Wed, 28 Feb 2024 13:11:48 +0100 Subject: [PATCH 1/5] Add fastapi_auth_api_key --- fastapi_auth_api_key/README.rst | 0 fastapi_auth_api_key/__init__.py | 1 + fastapi_auth_api_key/__manifest__.py | 20 + fastapi_auth_api_key/dependencies.py | 60 +++ fastapi_auth_api_key/models/__init__.py | 1 + .../models/fastapi_endpoint.py | 10 + fastapi_auth_api_key/readme/CONTRIBUTORS.rst | 1 + fastapi_auth_api_key/readme/DESCRIPTION.rst | 1 + fastapi_auth_api_key/readme/USAGE.rst | 33 ++ .../static/description/index.html | 369 ++++++++++++++++++ fastapi_auth_api_key/tests/__init__.py | 1 + .../test_fastapi_api_key_dependencies.py | 88 +++++ .../views/fastapi_endpoint.xml | 17 + 13 files changed, 602 insertions(+) create mode 100644 fastapi_auth_api_key/README.rst create mode 100644 fastapi_auth_api_key/__init__.py create mode 100644 fastapi_auth_api_key/__manifest__.py create mode 100644 fastapi_auth_api_key/dependencies.py create mode 100644 fastapi_auth_api_key/models/__init__.py create mode 100644 fastapi_auth_api_key/models/fastapi_endpoint.py create mode 100644 fastapi_auth_api_key/readme/CONTRIBUTORS.rst create mode 100644 fastapi_auth_api_key/readme/DESCRIPTION.rst create mode 100644 fastapi_auth_api_key/readme/USAGE.rst create mode 100644 fastapi_auth_api_key/static/description/index.html create mode 100644 fastapi_auth_api_key/tests/__init__.py create mode 100644 fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py create mode 100644 fastapi_auth_api_key/views/fastapi_endpoint.xml diff --git a/fastapi_auth_api_key/README.rst b/fastapi_auth_api_key/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/fastapi_auth_api_key/__init__.py b/fastapi_auth_api_key/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/fastapi_auth_api_key/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fastapi_auth_api_key/__manifest__.py b/fastapi_auth_api_key/__manifest__.py new file mode 100644 index 000000000..1735c59be --- /dev/null +++ b/fastapi_auth_api_key/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Fastapi Auth Api Key", + "version": "16.0.1.0.0", + "category": "Others", + "website": "https://github.com/OCA/rest-framework", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["mmequignon"], + "license": "AGPL-3", + "installable": True, + "depends": [ + "fastapi", + "auth_api_key_group", + ], + "data": [ + "views/fastapi_endpoint.xml", + ], +} diff --git a/fastapi_auth_api_key/dependencies.py b/fastapi_auth_api_key/dependencies.py new file mode 100644 index 000000000..3b71e6d58 --- /dev/null +++ b/fastapi_auth_api_key/dependencies.py @@ -0,0 +1,60 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from typing_extensions import Annotated + +from odoo import SUPERUSER_ID +from odoo.api import Environment +from odoo.exceptions import ValidationError + +from odoo.addons.auth_api_key.models.auth_api_key import AuthApiKey +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env +from odoo.addons.fastapi.models.fastapi_endpoint import FastapiEndpoint + +from fastapi import Depends, status +from fastapi.exceptions import HTTPException +from fastapi.security import APIKeyHeader + + +def authenticated_auth_api_key( + key: Annotated[str, Depends(APIKeyHeader(name="HTTP-API-KEY"))], + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], +) -> AuthApiKey: + if not key: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="No HTTP-API-KEY provided", + headers={"WWW-Authenticate": "HTTP-API-KEY"}, + ) + admin_env = Environment(env.cr, SUPERUSER_ID, {}) + try: + auth_api_key = admin_env["auth.api.key"]._retrieve_api_key(key) + except ValidationError as error: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=error.name, + headers={"WWW-Authenticate": "HTTP-API-KEY"}, + ) from error + # Ensure the api key is authorized for the current endpoint. + if auth_api_key not in endpoint.sudo().auth_api_key_group_id.auth_api_key_ids: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized", + headers={"WWW-Authenticate": "HTTP-API-KEY"}, + ) + return auth_api_key + + +def authenticated_partner_by_api_key( + auth_api_key: Annotated[AuthApiKey, Depends(authenticated_auth_api_key)] +) -> Partner: + return auth_api_key.user_id.partner_id + + +def authenticated_env_by_auth_api_key( + auth_api_key: Annotated[AuthApiKey, Depends(authenticated_auth_api_key)] +) -> Environment: + # set api key id in context + return auth_api_key.with_user(auth_api_key.user_id).env diff --git a/fastapi_auth_api_key/models/__init__.py b/fastapi_auth_api_key/models/__init__.py new file mode 100644 index 000000000..b825fab92 --- /dev/null +++ b/fastapi_auth_api_key/models/__init__.py @@ -0,0 +1 @@ +from . import fastapi_endpoint diff --git a/fastapi_auth_api_key/models/fastapi_endpoint.py b/fastapi_auth_api_key/models/fastapi_endpoint.py new file mode 100644 index 000000000..8da7ff66d --- /dev/null +++ b/fastapi_auth_api_key/models/fastapi_endpoint.py @@ -0,0 +1,10 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + auth_api_key_group_id = fields.Many2one("auth.api.key.group") diff --git a/fastapi_auth_api_key/readme/CONTRIBUTORS.rst b/fastapi_auth_api_key/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..bca4ee0ca --- /dev/null +++ b/fastapi_auth_api_key/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Matthieu Méquignon diff --git a/fastapi_auth_api_key/readme/DESCRIPTION.rst b/fastapi_auth_api_key/readme/DESCRIPTION.rst new file mode 100644 index 000000000..22e772df2 --- /dev/null +++ b/fastapi_auth_api_key/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Provides `FastAPI` dependencies for Api Key authentication. diff --git a/fastapi_auth_api_key/readme/USAGE.rst b/fastapi_auth_api_key/readme/USAGE.rst new file mode 100644 index 000000000..356cb02d2 --- /dev/null +++ b/fastapi_auth_api_key/readme/USAGE.rst @@ -0,0 +1,33 @@ +Getting an odoo environment +=========================== + +If you need to get an odoo env based on the provided api key, you can use `authenticated_env_by_auth_api_key`. + +.. code-block:: python + + @router.get("/example_with_authenticated_env") + def example_with_authenticated_env( + env: Annotated[Environment, Depends(authenticated_env_by_auth_api_key)], + ) -> None: + # env.user is the user attached to the provided key + pass + +Getting the authenticated partner +================================= + +If want to get the partned related to the the provided api key, you can use `authenticated_partner_by_api_key` + +.. code-block:: python + + @router.get("/example_with_authenticated_partner") + def example_with_authenticated_partner( + partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)], + ) -> None: + # partner is the partner related to the provided key key.user_id.partner_id + pass + +Configuration +============= + +For this to work, the api key must be defined on the `Endpoint`. +A new field `auth_api_key_group_id` has been added to the `Endpoint` model. diff --git a/fastapi_auth_api_key/static/description/index.html b/fastapi_auth_api_key/static/description/index.html new file mode 100644 index 000000000..ec3643de8 --- /dev/null +++ b/fastapi_auth_api_key/static/description/index.html @@ -0,0 +1,369 @@ + + + + + + +README.rst + + + +
+ + + +
+ + diff --git a/fastapi_auth_api_key/tests/__init__.py b/fastapi_auth_api_key/tests/__init__.py new file mode 100644 index 000000000..da7258cc3 --- /dev/null +++ b/fastapi_auth_api_key/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_api_key_dependencies diff --git a/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py new file mode 100644 index 000000000..d746e1ccc --- /dev/null +++ b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py @@ -0,0 +1,88 @@ +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + +from fastapi.exceptions import HTTPException + +from ..dependencies import ( + authenticated_auth_api_key, + authenticated_env_by_auth_api_key, + authenticated_partner_by_api_key, +) + + +@tagged("-at_install", "post_install") +class TestFastapiAuthApiKey(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + # Is it valid? We need an env without superpowers + demo_user = cls.env.ref("base.partner_demo") + cls.demo_env = demo_user.with_user(demo_user).env + cls.setUpClassApiKey() + + @classmethod + def setUpClassApiKey(cls): + user_model = cls.env["res.users"].with_context(no_reset_password=True) + cls.authorized_user = user_model.create( + { + "name": "John Authorized", + "login": "johnauth", + } + ) + cls.unauthorized_user = user_model.create( + { + "name": "Bob Unauthorized", + "login": "bobunauth", + } + ) + api_key_model = cls.env["auth.api.key"] + cls.authorized_api_key = api_key_model.create( + { + "user_id": cls.authorized_user.id, + "name": "Authorized api key", + "key": "authorized_key", + } + ) + cls.unauthorized_api_key = api_key_model.create( + { + "user_id": cls.unauthorized_user.id, + "name": "Unauthorized api key", + "key": "unauthorized_key", + } + ) + api_key_group_model = cls.env["auth.api.key.group"] + cls.authorized_api_key_group = api_key_group_model.create( + { + "name": "Authorized api key group", + "code": "authorized_api_key_group", + "auth_api_key_ids": [(6, 0, cls.authorized_api_key.ids)], + } + ) + + def test_authenticated_auth_api_key(self): + # An exception is raised when no api key is used + with self.assertRaises(HTTPException) as error: + authenticated_auth_api_key(False, self.demo_env) + self.assertEqual(error.exception.detail, "No HTTP-API-KEY provided") + # An exception is raised when no api key record is found + with self.assertRaises(HTTPException) as error: + authenticated_auth_api_key("404", self.demo_env) + self.assertEqual(error.exception.detail, "The key 404 is not allowed") + # TODO enable this when we know how to filter keys based + # on endpoint's api key group. + # An exception is raised when unauthorized api key record is found + # with self.assertRaises(HTTPException) as error: + # authenticated_auth_api_key("not_authorized", self.demo_env) + result_key = authenticated_auth_api_key( + self.authorized_api_key.key, self.demo_env + ) + self.assertEqual(result_key, self.authorized_api_key) + + def test_authenticated_partner_by_api_key(self): + result_partner = authenticated_partner_by_api_key(self.authorized_api_key) + self.assertEqual(result_partner, self.authorized_user.partner_id) + + def test_authenticated_env_by_auth_api_key(self): + result_env = authenticated_env_by_auth_api_key(self.authorized_api_key) + self.assertEqual(result_env.user, self.authorized_user) diff --git a/fastapi_auth_api_key/views/fastapi_endpoint.xml b/fastapi_auth_api_key/views/fastapi_endpoint.xml new file mode 100644 index 000000000..bd8ba2a90 --- /dev/null +++ b/fastapi_auth_api_key/views/fastapi_endpoint.xml @@ -0,0 +1,17 @@ + + + + + + fastapi.endpoint.form.inherit + fastapi.endpoint + + + + + + + + + From ff3f1ddc8dc32784234149879f162124fc6630f0 Mon Sep 17 00:00:00 2001 From: sonhd91 Date: Fri, 24 May 2024 18:30:22 +0700 Subject: [PATCH 2/5] [IMP] fastapi_auth_api_key: pre-commit auto fixes --- fastapi_auth_api_key/README.rst | 121 ++++++++++++++++++ .../odoo/addons/fastapi_auth_api_key | 1 + setup/fastapi_auth_api_key/setup.py | 6 + 3 files changed, 128 insertions(+) create mode 120000 setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key create mode 100644 setup/fastapi_auth_api_key/setup.py diff --git a/fastapi_auth_api_key/README.rst b/fastapi_auth_api_key/README.rst index e69de29bb..bd0a9aaf7 100644 --- a/fastapi_auth_api_key/README.rst +++ b/fastapi_auth_api_key/README.rst @@ -0,0 +1,121 @@ +==================== +Fastapi Auth Api Key +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a1a8681b1c3e7a13dc83e2e61a1d78ad8c8da1ddb684c8cf563607e96cf4f7e7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_auth_api_key + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Provides `FastAPI` dependencies for Api Key authentication. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Getting an odoo environment +=========================== + +If you need to get an odoo env based on the provided api key, you can use `authenticated_env_by_auth_api_key`. + +.. code-block:: python + + @router.get("/example_with_authenticated_env") + def example_with_authenticated_env( + env: Annotated[Environment, Depends(authenticated_env_by_auth_api_key)], + ) -> None: + # env.user is the user attached to the provided key + pass + +Getting the authenticated partner +================================= + +If want to get the partned related to the the provided api key, you can use `authenticated_partner_by_api_key` + +.. code-block:: python + + @router.get("/example_with_authenticated_partner") + def example_with_authenticated_partner( + partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)], + ) -> None: + # partner is the partner related to the provided key key.user_id.partner_id + pass + +Configuration +============= + +For this to work, the api key must be defined on the `Endpoint`. +A new field `auth_api_key_group_id` has been added to the `Endpoint` model. + +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 +~~~~~~~~~~~~ + +* Matthieu Méquignon + +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-mmequignon| image:: https://github.com/mmequignon.png?size=40px + :target: https://github.com/mmequignon + :alt: mmequignon + +Current `maintainer `__: + +|maintainer-mmequignon| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key b/setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key new file mode 120000 index 000000000..498f5ddb6 --- /dev/null +++ b/setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key @@ -0,0 +1 @@ +../../../../fastapi_auth_api_key \ No newline at end of file diff --git a/setup/fastapi_auth_api_key/setup.py b/setup/fastapi_auth_api_key/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_auth_api_key/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From edbe2ab6d30e9588fbd1afcbf4a8d0ab6bcbf228 Mon Sep 17 00:00:00 2001 From: sonhd91 Date: Fri, 24 May 2024 18:23:29 +0700 Subject: [PATCH 3/5] [MIG] fastapi_auth_api_key: Migration to 17.0 --- fastapi_auth_api_key/README.rst | 71 +++++++++++-------- fastapi_auth_api_key/__manifest__.py | 2 +- fastapi_auth_api_key/dependencies.py | 4 +- fastapi_auth_api_key/pyproject.toml | 3 + fastapi_auth_api_key/readme/CONTRIBUTORS.md | 2 + fastapi_auth_api_key/readme/CONTRIBUTORS.rst | 1 - fastapi_auth_api_key/readme/CREDITS.md | 2 + fastapi_auth_api_key/readme/DESCRIPTION.md | 1 + fastapi_auth_api_key/readme/DESCRIPTION.rst | 1 - fastapi_auth_api_key/readme/USAGE.md | 32 +++++++++ fastapi_auth_api_key/readme/USAGE.rst | 33 --------- .../test_fastapi_api_key_dependencies.py | 22 ++++-- .../views/fastapi_endpoint.xml | 2 +- 13 files changed, 101 insertions(+), 75 deletions(-) create mode 100644 fastapi_auth_api_key/pyproject.toml create mode 100644 fastapi_auth_api_key/readme/CONTRIBUTORS.md delete mode 100644 fastapi_auth_api_key/readme/CONTRIBUTORS.rst create mode 100644 fastapi_auth_api_key/readme/CREDITS.md create mode 100644 fastapi_auth_api_key/readme/DESCRIPTION.md delete mode 100644 fastapi_auth_api_key/readme/DESCRIPTION.rst create mode 100644 fastapi_auth_api_key/readme/USAGE.md delete mode 100644 fastapi_auth_api_key/readme/USAGE.rst diff --git a/fastapi_auth_api_key/README.rst b/fastapi_auth_api_key/README.rst index bd0a9aaf7..f97f63c53 100644 --- a/fastapi_auth_api_key/README.rst +++ b/fastapi_auth_api_key/README.rst @@ -17,18 +17,18 @@ Fastapi Auth Api Key :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_auth_api_key + :target: https://github.com/OCA/rest-framework/tree/17.0/fastapi_auth_api_key :alt: OCA/rest-framework .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_auth_api_key + :target: https://translation.odoo-community.org/projects/rest-framework-17-0/rest-framework-17-0-fastapi_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/rest-framework&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| -Provides `FastAPI` dependencies for Api Key authentication. +Provides FastAPI dependencies for Api Key authentication. **Table of contents** @@ -39,38 +39,40 @@ Usage ===== Getting an odoo environment -=========================== +--------------------------- -If you need to get an odoo env based on the provided api key, you can use `authenticated_env_by_auth_api_key`. +If you need to get an odoo env based on the provided api key, you can +use authenticated_env_by_auth_api_key. -.. code-block:: python +.. code:: python - @router.get("/example_with_authenticated_env") - def example_with_authenticated_env( - env: Annotated[Environment, Depends(authenticated_env_by_auth_api_key)], - ) -> None: - # env.user is the user attached to the provided key - pass + @router.get("/example_with_authenticated_env") + def example_with_authenticated_env( + env: Annotated[Environment, Depends(authenticated_env_by_auth_api_key)], + ) -> None: + # env.user is the user attached to the provided key + pass Getting the authenticated partner -================================= +--------------------------------- -If want to get the partned related to the the provided api key, you can use `authenticated_partner_by_api_key` +If want to get the partned related to the the provided api key, you can +use authenticated_partner_by_api_key -.. code-block:: python +.. code:: python - @router.get("/example_with_authenticated_partner") - def example_with_authenticated_partner( - partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)], - ) -> None: - # partner is the partner related to the provided key key.user_id.partner_id - pass + @router.get("/example_with_authenticated_partner") + def example_with_authenticated_partner( + partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)], + ) -> None: + # partner is the partner related to the provided key key.user_id.partner_id + pass Configuration -============= +------------- -For this to work, the api key must be defined on the `Endpoint`. -A new field `auth_api_key_group_id` has been added to the `Endpoint` model. +For this to work, the api key must be defined on the Endpoint. A new +field auth_api_key_group_id has been added to the Endpoint model. Bug Tracker =========== @@ -78,7 +80,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -86,17 +88,24 @@ Credits ======= Authors -~~~~~~~ +------- * Camptocamp Contributors -~~~~~~~~~~~~ +------------ -* Matthieu Méquignon +- Matthieu Méquignon +- Son Ho + +Other credits +------------- + +The migration of this module from 16.0 to 17.0 was financially supported +by Camptocamp Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -116,6 +125,6 @@ Current `maintainer `__: |maintainer-mmequignon| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_auth_api_key/__manifest__.py b/fastapi_auth_api_key/__manifest__.py index 1735c59be..539efdcc1 100644 --- a/fastapi_auth_api_key/__manifest__.py +++ b/fastapi_auth_api_key/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Fastapi Auth Api Key", - "version": "16.0.1.0.0", + "version": "17.0.1.0.0", "category": "Others", "website": "https://github.com/OCA/rest-framework", "author": "Camptocamp, Odoo Community Association (OCA)", diff --git a/fastapi_auth_api_key/dependencies.py b/fastapi_auth_api_key/dependencies.py index 3b71e6d58..aa1838ab0 100644 --- a/fastapi_auth_api_key/dependencies.py +++ b/fastapi_auth_api_key/dependencies.py @@ -1,7 +1,7 @@ # Copyright 2024 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from typing_extensions import Annotated +from typing import Annotated from odoo import SUPERUSER_ID from odoo.api import Environment @@ -34,7 +34,7 @@ def authenticated_auth_api_key( except ValidationError as error: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=error.name, + detail=error.args, headers={"WWW-Authenticate": "HTTP-API-KEY"}, ) from error # Ensure the api key is authorized for the current endpoint. diff --git a/fastapi_auth_api_key/pyproject.toml b/fastapi_auth_api_key/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/fastapi_auth_api_key/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fastapi_auth_api_key/readme/CONTRIBUTORS.md b/fastapi_auth_api_key/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..a6f2fed34 --- /dev/null +++ b/fastapi_auth_api_key/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Matthieu Méquignon \<\> +- Son Ho \<\> diff --git a/fastapi_auth_api_key/readme/CONTRIBUTORS.rst b/fastapi_auth_api_key/readme/CONTRIBUTORS.rst deleted file mode 100644 index bca4ee0ca..000000000 --- a/fastapi_auth_api_key/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1 +0,0 @@ -* Matthieu Méquignon diff --git a/fastapi_auth_api_key/readme/CREDITS.md b/fastapi_auth_api_key/readme/CREDITS.md new file mode 100644 index 000000000..7c48f8e03 --- /dev/null +++ b/fastapi_auth_api_key/readme/CREDITS.md @@ -0,0 +1,2 @@ +The migration of this module from 16.0 to 17.0 was financially supported +by Camptocamp diff --git a/fastapi_auth_api_key/readme/DESCRIPTION.md b/fastapi_auth_api_key/readme/DESCRIPTION.md new file mode 100644 index 000000000..1e9aa6910 --- /dev/null +++ b/fastapi_auth_api_key/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Provides FastAPI dependencies for Api Key authentication. diff --git a/fastapi_auth_api_key/readme/DESCRIPTION.rst b/fastapi_auth_api_key/readme/DESCRIPTION.rst deleted file mode 100644 index 22e772df2..000000000 --- a/fastapi_auth_api_key/readme/DESCRIPTION.rst +++ /dev/null @@ -1 +0,0 @@ -Provides `FastAPI` dependencies for Api Key authentication. diff --git a/fastapi_auth_api_key/readme/USAGE.md b/fastapi_auth_api_key/readme/USAGE.md new file mode 100644 index 000000000..f60799c41 --- /dev/null +++ b/fastapi_auth_api_key/readme/USAGE.md @@ -0,0 +1,32 @@ +## Getting an odoo environment + +If you need to get an odoo env based on the provided api key, you can +use authenticated_env_by_auth_api_key. + +``` python +@router.get("/example_with_authenticated_env") +def example_with_authenticated_env( + env: Annotated[Environment, Depends(authenticated_env_by_auth_api_key)], +) -> None: + # env.user is the user attached to the provided key + pass +``` + +## Getting the authenticated partner + +If want to get the partned related to the the provided api key, you can +use authenticated_partner_by_api_key + +``` python +@router.get("/example_with_authenticated_partner") +def example_with_authenticated_partner( + partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)], +) -> None: + # partner is the partner related to the provided key key.user_id.partner_id + pass +``` + +## Configuration + +For this to work, the api key must be defined on the Endpoint. A new +field auth_api_key_group_id has been added to the Endpoint model. diff --git a/fastapi_auth_api_key/readme/USAGE.rst b/fastapi_auth_api_key/readme/USAGE.rst deleted file mode 100644 index 356cb02d2..000000000 --- a/fastapi_auth_api_key/readme/USAGE.rst +++ /dev/null @@ -1,33 +0,0 @@ -Getting an odoo environment -=========================== - -If you need to get an odoo env based on the provided api key, you can use `authenticated_env_by_auth_api_key`. - -.. code-block:: python - - @router.get("/example_with_authenticated_env") - def example_with_authenticated_env( - env: Annotated[Environment, Depends(authenticated_env_by_auth_api_key)], - ) -> None: - # env.user is the user attached to the provided key - pass - -Getting the authenticated partner -================================= - -If want to get the partned related to the the provided api key, you can use `authenticated_partner_by_api_key` - -.. code-block:: python - - @router.get("/example_with_authenticated_partner") - def example_with_authenticated_partner( - partner: Annotated[Partner, Depends(authenticated_partner_by_api_key)], - ) -> None: - # partner is the partner related to the provided key key.user_id.partner_id - pass - -Configuration -============= - -For this to work, the api key must be defined on the `Endpoint`. -A new field `auth_api_key_group_id` has been added to the `Endpoint` model. diff --git a/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py index d746e1ccc..dde6088ca 100644 --- a/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py +++ b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py @@ -17,8 +17,17 @@ def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) # Is it valid? We need an env without superpowers - demo_user = cls.env.ref("base.partner_demo") + demo_user = cls.env.ref("base.user_demo") cls.demo_env = demo_user.with_user(demo_user).env + cls.demo_endpoint = cls.env["fastapi.endpoint"].create( + { + "name": "Test Enpoint", + "app": "demo", + "root_path": "/path_demo", + "demo_auth_method": "api_key", + "user_id": demo_user.id, + } + ) cls.setUpClassApiKey() @classmethod @@ -63,19 +72,22 @@ def setUpClassApiKey(cls): def test_authenticated_auth_api_key(self): # An exception is raised when no api key is used with self.assertRaises(HTTPException) as error: - authenticated_auth_api_key(False, self.demo_env) + authenticated_auth_api_key(False, self.demo_env, self.demo_endpoint) self.assertEqual(error.exception.detail, "No HTTP-API-KEY provided") # An exception is raised when no api key record is found with self.assertRaises(HTTPException) as error: - authenticated_auth_api_key("404", self.demo_env) - self.assertEqual(error.exception.detail, "The key 404 is not allowed") + authenticated_auth_api_key("404", self.demo_env, self.demo_endpoint) + self.assertEqual(error.exception.detail, ("The key 404 is not allowed",)) # TODO enable this when we know how to filter keys based # on endpoint's api key group. # An exception is raised when unauthorized api key record is found # with self.assertRaises(HTTPException) as error: # authenticated_auth_api_key("not_authorized", self.demo_env) + self.demo_endpoint.auth_api_key_group_id = ( + self.authorized_api_key.auth_api_key_group_ids[0] + ) result_key = authenticated_auth_api_key( - self.authorized_api_key.key, self.demo_env + self.authorized_api_key.key, self.demo_env, self.demo_endpoint ) self.assertEqual(result_key, self.authorized_api_key) diff --git a/fastapi_auth_api_key/views/fastapi_endpoint.xml b/fastapi_auth_api_key/views/fastapi_endpoint.xml index bd8ba2a90..1005dd501 100644 --- a/fastapi_auth_api_key/views/fastapi_endpoint.xml +++ b/fastapi_auth_api_key/views/fastapi_endpoint.xml @@ -8,7 +8,7 @@ fastapi.endpoint - + From 7c7b37c0cbcc150da650047cac762128a8eb95a8 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Tue, 29 Jul 2025 11:11:20 +0200 Subject: [PATCH 4/5] [REF] fastapi_auth_api_key: review improvements --- fastapi_auth_api_key/README.rst | 6 +-- fastapi_auth_api_key/__manifest__.py | 2 +- fastapi_auth_api_key/dependencies.py | 24 +++++++----- .../models/fastapi_endpoint.py | 5 ++- .../test_fastapi_api_key_dependencies.py | 39 ++++++++++++------- .../odoo/addons/fastapi_auth_api_key | 1 - setup/fastapi_auth_api_key/setup.py | 6 --- 7 files changed, 48 insertions(+), 35 deletions(-) delete mode 120000 setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key delete mode 100644 setup/fastapi_auth_api_key/setup.py diff --git a/fastapi_auth_api_key/README.rst b/fastapi_auth_api_key/README.rst index f97f63c53..eb824b8ef 100644 --- a/fastapi_auth_api_key/README.rst +++ b/fastapi_auth_api_key/README.rst @@ -1,5 +1,5 @@ ==================== -Fastapi Auth Api Key +Fastapi Auth API Key ==================== .. @@ -95,8 +95,8 @@ Authors Contributors ------------ -- Matthieu Méquignon -- Son Ho +- Matthieu Méquignon +- Son Ho Other credits ------------- diff --git a/fastapi_auth_api_key/__manifest__.py b/fastapi_auth_api_key/__manifest__.py index 539efdcc1..f6136a6fe 100644 --- a/fastapi_auth_api_key/__manifest__.py +++ b/fastapi_auth_api_key/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) { - "name": "Fastapi Auth Api Key", + "name": "Fastapi Auth API Key", "version": "17.0.1.0.0", "category": "Others", "website": "https://github.com/OCA/rest-framework", diff --git a/fastapi_auth_api_key/dependencies.py b/fastapi_auth_api_key/dependencies.py index aa1838ab0..390c93a37 100644 --- a/fastapi_auth_api_key/dependencies.py +++ b/fastapi_auth_api_key/dependencies.py @@ -1,9 +1,9 @@ # Copyright 2024 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - +import os from typing import Annotated -from odoo import SUPERUSER_ID +from odoo import SUPERUSER_ID, _ from odoo.api import Environment from odoo.exceptions import ValidationError @@ -16,17 +16,20 @@ from fastapi.exceptions import HTTPException from fastapi.security import APIKeyHeader +HTTP_API_KEY_HEADER = os.environ.get("FASTAPI_AUTH_HTTP_API_KEY_HEADER", "HTTP-API-KEY") + def authenticated_auth_api_key( - key: Annotated[str, Depends(APIKeyHeader(name="HTTP-API-KEY"))], + key: Annotated[str, Depends(APIKeyHeader(name=HTTP_API_KEY_HEADER))], env: Annotated[Environment, Depends(odoo_env)], endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], ) -> AuthApiKey: if not key: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="No HTTP-API-KEY provided", - headers={"WWW-Authenticate": "HTTP-API-KEY"}, + detail=_("Missing %(HTTP_API_KEY_HEADER)s header") + % {"HTTP_API_KEY_HEADER": HTTP_API_KEY_HEADER}, + headers={"WWW-Authenticate": HTTP_API_KEY_HEADER}, ) admin_env = Environment(env.cr, SUPERUSER_ID, {}) try: @@ -35,14 +38,17 @@ def authenticated_auth_api_key( raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=error.args, - headers={"WWW-Authenticate": "HTTP-API-KEY"}, + headers={"WWW-Authenticate": HTTP_API_KEY_HEADER}, ) from error # Ensure the api key is authorized for the current endpoint. - if auth_api_key not in endpoint.sudo().auth_api_key_group_id.auth_api_key_ids: + if ( + endpoint.sudo().auth_api_key_group_id + and auth_api_key not in endpoint.sudo().auth_api_key_group_id.auth_api_key_ids + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Unauthorized", - headers={"WWW-Authenticate": "HTTP-API-KEY"}, + detail=_("Unauthorized"), + headers={"WWW-Authenticate": HTTP_API_KEY_HEADER}, ) return auth_api_key diff --git a/fastapi_auth_api_key/models/fastapi_endpoint.py b/fastapi_auth_api_key/models/fastapi_endpoint.py index 8da7ff66d..697717ff5 100644 --- a/fastapi_auth_api_key/models/fastapi_endpoint.py +++ b/fastapi_auth_api_key/models/fastapi_endpoint.py @@ -7,4 +7,7 @@ class FastapiEndpoint(models.Model): _inherit = "fastapi.endpoint" - auth_api_key_group_id = fields.Many2one("auth.api.key.group") + auth_api_key_group_id = fields.Many2one( + "auth.api.key.group", + help="If not set, all 'auth.api.key' are allowed to access to endpoints", + ) diff --git a/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py index dde6088ca..54a297f2f 100644 --- a/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py +++ b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py @@ -1,14 +1,14 @@ from odoo.tests import tagged from odoo.tests.common import TransactionCase -from fastapi.exceptions import HTTPException - -from ..dependencies import ( +from odoo.addons.fastapi_auth_api_key.dependencies import ( authenticated_auth_api_key, authenticated_env_by_auth_api_key, authenticated_partner_by_api_key, ) +from fastapi.exceptions import HTTPException + @tagged("-at_install", "post_install") class TestFastapiAuthApiKey(TransactionCase): @@ -16,7 +16,6 @@ class TestFastapiAuthApiKey(TransactionCase): def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - # Is it valid? We need an env without superpowers demo_user = cls.env.ref("base.user_demo") cls.demo_env = demo_user.with_user(demo_user).env cls.demo_endpoint = cls.env["fastapi.endpoint"].create( @@ -68,29 +67,41 @@ def setUpClassApiKey(cls): "auth_api_key_ids": [(6, 0, cls.authorized_api_key.ids)], } ) + cls.demo_endpoint.auth_api_key_group_id = cls.authorized_api_key_group - def test_authenticated_auth_api_key(self): + def test_authenticated_auth_api_key_no_api_key(self): # An exception is raised when no api key is used with self.assertRaises(HTTPException) as error: authenticated_auth_api_key(False, self.demo_env, self.demo_endpoint) - self.assertEqual(error.exception.detail, "No HTTP-API-KEY provided") + self.assertEqual(error.exception.detail, "Missing HTTP-API-KEY header") + + def test_authenticated_auth_api_key_no_api_key_found(self): # An exception is raised when no api key record is found with self.assertRaises(HTTPException) as error: authenticated_auth_api_key("404", self.demo_env, self.demo_endpoint) self.assertEqual(error.exception.detail, ("The key 404 is not allowed",)) - # TODO enable this when we know how to filter keys based - # on endpoint's api key group. + + def test_authenticated_auth_api_key_unauthorized_key(self): # An exception is raised when unauthorized api key record is found - # with self.assertRaises(HTTPException) as error: - # authenticated_auth_api_key("not_authorized", self.demo_env) - self.demo_endpoint.auth_api_key_group_id = ( - self.authorized_api_key.auth_api_key_group_ids[0] - ) + with self.assertRaisesRegex(HTTPException, r"Unauthorized"): + authenticated_auth_api_key( + "unauthorized_key", self.demo_env, self.demo_endpoint + ) + + def test_authenticated_auth_api_key(self): result_key = authenticated_auth_api_key( - self.authorized_api_key.key, self.demo_env, self.demo_endpoint + "authorized_key", self.demo_env, self.demo_endpoint ) self.assertEqual(result_key, self.authorized_api_key) + def test_authenticated_auth_api_key_without_group(self): + # test with no group set unauthorized is allow to access + self.demo_endpoint.auth_api_key_group_id = False + result_key = authenticated_auth_api_key( + "unauthorized_key", self.demo_env, self.demo_endpoint + ) + self.assertEqual(result_key, self.unauthorized_api_key) + def test_authenticated_partner_by_api_key(self): result_partner = authenticated_partner_by_api_key(self.authorized_api_key) self.assertEqual(result_partner, self.authorized_user.partner_id) diff --git a/setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key b/setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key deleted file mode 120000 index 498f5ddb6..000000000 --- a/setup/fastapi_auth_api_key/odoo/addons/fastapi_auth_api_key +++ /dev/null @@ -1 +0,0 @@ -../../../../fastapi_auth_api_key \ No newline at end of file diff --git a/setup/fastapi_auth_api_key/setup.py b/setup/fastapi_auth_api_key/setup.py deleted file mode 100644 index 28c57bb64..000000000 --- a/setup/fastapi_auth_api_key/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -) From 4e547bcfe666c4842833e0beb038f6a3f5970ae4 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Mon, 8 Sep 2025 14:41:20 +0200 Subject: [PATCH 5/5] [FIX] freeze graphql_server --- graphql_base/README.rst | 28 +++++++++++----------- graphql_base/__manifest__.py | 2 +- graphql_base/static/description/index.html | 16 ++++++------- requirements.txt | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/graphql_base/README.rst b/graphql_base/README.rst index 0b18d4bd2..6721d9a72 100644 --- a/graphql_base/README.rst +++ b/graphql_base/README.rst @@ -44,9 +44,9 @@ Usage To use this module, you need to -- create your graphene schema -- create your controller to expose your GraphQL endpoint, and - optionally a GraphiQL IDE. +- create your graphene schema +- create your controller to expose your GraphQL endpoint, and optionally + a GraphiQL IDE. This module does not attempt to expose the whole Odoo object model. This could be the purpose of another module based on this one. We believe @@ -56,12 +56,12 @@ exposed and needs to be tested when upgrading Odoo. To start working with this module, we recommend the following approach: -- Learn `GraphQL basics `__ -- Learn `graphene `__, the python library - used to create GraphQL schemas and resolvers. -- Examine the ``graphql_demo`` module in this repo, copy it, adapt the - controller to suit your needs (routes, authentication methods). -- Start building your own schema and resolver. +- Learn `GraphQL basics `__ +- Learn `graphene `__, the python library + used to create GraphQL schemas and resolvers. +- Examine the ``graphql_demo`` module in this repo, copy it, adapt the + controller to suit your needs (routes, authentication methods). +- Start building your own schema and resolver. Building your schema -------------------- @@ -71,11 +71,11 @@ The schema can be built using native graphene types. An convenience. It is a graphene ``ObjectType`` with a default attribute resolver which: -- converts False to None (except for Boolean types), to avoid Odoo's - weird ``False`` strings being rendered as json ``"false"``; -- adds the user timezone to Datetime fields; -- raises an error if an attribute is absent to avoid field name typing - errors. +- converts False to None (except for Boolean types), to avoid Odoo's + weird ``False`` strings being rendered as json ``"false"``; +- adds the user timezone to Datetime fields; +- raises an error if an attribute is absent to avoid field name typing + errors. Creating GraphQL controllers ---------------------------- diff --git a/graphql_base/__manifest__.py b/graphql_base/__manifest__.py index e4f3a1c38..756f5b763 100644 --- a/graphql_base/__manifest__.py +++ b/graphql_base/__manifest__.py @@ -11,7 +11,7 @@ "website": "https://github.com/OCA/rest-framework", "depends": ["base"], "data": ["views/graphiql.xml"], - "external_dependencies": {"python": ["graphene", "graphql_server"]}, + "external_dependencies": {"python": ["graphene", "graphql_server==3.0.0b7"]}, "development_status": "Production/Stable", "maintainers": ["sbidoul"], "installable": True, diff --git a/graphql_base/static/description/index.html b/graphql_base/static/description/index.html index b28702051..b55217acd 100644 --- a/graphql_base/static/description/index.html +++ b/graphql_base/static/description/index.html @@ -396,8 +396,8 @@

Usage

To use this module, you need to

  • create your graphene schema
  • -
  • create your controller to expose your GraphQL endpoint, and -optionally a GraphiQL IDE.
  • +
  • create your controller to expose your GraphQL endpoint, and optionally +a GraphiQL IDE.

This module does not attempt to expose the whole Odoo object model. This could be the purpose of another module based on this one. We believe @@ -433,24 +433,24 @@

Creating GraphQL controllers< odoo.addons.graphql_base.GraphQLControllerMixin class to help you build GraphQL controllers providing GraphiQL and/or GraphQL endpoints.

-from odoo import http
-from odoo.addons.graphql_base import GraphQLControllerMixin
+from odoo import http
+from odoo.addons.graphql_base import GraphQLControllerMixin
 
-from ..schema import schema
+from ..schema import schema
 
 
-class GraphQLController(http.Controller, GraphQLControllerMixin):
+class GraphQLController(http.Controller, GraphQLControllerMixin):
 
     # The GraphiQL route, providing an IDE for developers
     @http.route("/graphiql/demo", auth="user")
-    def graphiql(self, **kwargs):
+    def graphiql(self, **kwargs):
         return self._handle_graphiql_request(schema)
 
     # The graphql route, for applications.
     # Note csrf=False: you may want to apply extra security
     # (such as origin restrictions) to this route.
     @http.route("/graphql/demo", auth="user", csrf=False)
-    def graphql(self, **kwargs):
+    def graphql(self, **kwargs):
         return self._handle_graphql_request(schema)
 
diff --git a/requirements.txt b/requirements.txt index a4ccc4e88..de9981355 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ a2wsgi>=1.10.6 contextvars fastapi>=0.110.0 graphene -graphql_server +graphql_server==3.0.0b7 parse-accept-language pydantic python-multipart