diff --git a/fastapi_auth_api_key/README.rst b/fastapi_auth_api_key/README.rst
new file mode 100644
index 000000000..eb824b8ef
--- /dev/null
+++ b/fastapi_auth_api_key/README.rst
@@ -0,0 +1,130 @@
+====================
+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/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-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=17.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:: 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:: 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
+- 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.
+
+.. 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/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..f6136a6fe
--- /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": "17.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..390c93a37
--- /dev/null
+++ b/fastapi_auth_api_key/dependencies.py
@@ -0,0 +1,66 @@
+# 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.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
+
+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_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=_("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:
+ 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.args,
+ headers={"WWW-Authenticate": HTTP_API_KEY_HEADER},
+ ) from error
+ # Ensure the api key is authorized for the current endpoint.
+ 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_HEADER},
+ )
+ 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..697717ff5
--- /dev/null
+++ b/fastapi_auth_api_key/models/fastapi_endpoint.py
@@ -0,0 +1,13 @@
+# 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",
+ help="If not set, all 'auth.api.key' are allowed to access to endpoints",
+ )
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/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/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/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..54a297f2f
--- /dev/null
+++ b/fastapi_auth_api_key/tests/test_fastapi_api_key_dependencies.py
@@ -0,0 +1,111 @@
+from odoo.tests import tagged
+from odoo.tests.common import TransactionCase
+
+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):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
+ 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
+ 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)],
+ }
+ )
+ cls.demo_endpoint.auth_api_key_group_id = cls.authorized_api_key_group
+
+ 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, "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",))
+
+ def test_authenticated_auth_api_key_unauthorized_key(self):
+ # An exception is raised when unauthorized api key record is found
+ 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(
+ "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)
+
+ 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..1005dd501
--- /dev/null
+++ b/fastapi_auth_api_key/views/fastapi_endpoint.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ fastapi.endpoint.form.inherit
+ fastapi.endpoint
+
+
+
+
+
+
+
+
+
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 @@
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