diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 388e56a6d..3618033de 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -67,6 +67,7 @@ class FastapiEndpoint(models.Model): default=True, ) route_group = fields.Char(help="Use this to classify routes together", size=32) + expose_doc_urls = fields.Boolean("Expose FastAPI docs", default=True) @api.depends("root_path") def _compute_root_path(self): @@ -152,7 +153,7 @@ def _handle_route_updates(self, vals): @api.model def _fastapi_app_fields(self) -> list[str]: """The list of fields requiring to refresh the fastapi app if modified""" - return [] + return ["expose_doc_urls"] def _make_routing_rule(self, options=None): """Generator of rule""" @@ -323,12 +324,15 @@ def _get_app_dependencies_overrides(self) -> dict[Callable, Callable]: def _prepare_fastapi_app_params(self) -> dict[str, Any]: """Return the params to pass to the Fast API app constructor""" - return { + to_return = { "title": self.name, "description": self.description, "middleware": self._get_fastapi_app_middlewares(), "dependencies": self._get_fastapi_app_dependencies(), } + if not self.expose_doc_urls: + to_return |= {"docs_url": None, "redoc_url": None, "openapi_url": None} + return to_return def _get_fastapi_routers(self) -> list[APIRouter]: """Return the api routers to use for the instance. diff --git a/fastapi/readme/USAGE.md b/fastapi/readme/USAGE.md index 131d7f7b5..c53513a9d 100644 --- a/fastapi/readme/USAGE.md +++ b/fastapi/readme/USAGE.md @@ -178,6 +178,12 @@ Now, you can start your Odoo server, install your addon and create a new endpoint instance for your app. Once it's done click on the docs url to access the interactive documentation of your app. +**Note**: FastAPI automatically exposes three services to easily +interact with the APIs and their structure (Swagger, Redoc, OpenAPI). +When exposing your Odoo instance to the open internet you might want to +disable these services. To do so toggle the "Expose FastAPI docs" +checkbox in the FastAPI Endpoint form view. + Before trying to test your app, you need to define on the endpoint instance the user that will be used to run the app. You can do it by setting the **'user_id'** field. This information is the most important diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index e074325e0..788461ca1 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -4,6 +4,7 @@ import os import unittest from contextlib import contextmanager +from pathlib import Path from odoo.tests.common import HttpCase from odoo.tests.test_cursor import TestCursor @@ -46,6 +47,17 @@ def _assert_expected_lang(self, accept_language, expected_lang): self.assertEqual(response.status_code, 200) self.assertEqual(response.content, expected_lang) + def endpoint_path(self, endpoint, path): + """Properly contatenate and endpoint's root_path and another path""" + if path[0] == "/": + # concatenating two paths where the second one starts with a '/' makes the + # resulting path exactly the same as this second path + # eg. Path("/root") / Path("/endpoint") -> PosixPath("/endpoint") + # We want to avoid that, we should get a result like + # -> PosixPath("/root/endpoint") + path = path[1:] + return str(Path(endpoint.root_path) / path) + def test_call(self): route = "/fastapi_demo/demo/" response = self.url_open(route) @@ -80,6 +92,36 @@ def test_retrying_post(self): self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"retries": nbr_retries, "file": "test"}) + def test_expose_docs(self): + self.assertNotIn( + "docs_url", self.fastapi_demo_app._prepare_fastapi_app_params().keys() + ) + response = self.url_open( + self.endpoint_path(self.fastapi_demo_app, "/docs"), timeout=20 + ) + self.assertEqual(response.status_code, 200) + + unexposed_endpoint = self.env["fastapi.endpoint"].create( + { + "name": "Test Endpoint - non exposed", + "root_path": "/test-endpoint/", + "app": "demo", + "demo_auth_method": "api_key", + "expose_doc_urls": False, + } + ) + self.assertIn( + "docs_url", unexposed_endpoint._prepare_fastapi_app_params().keys() + ) + self.assertEqual( + unexposed_endpoint._prepare_fastapi_app_params().get("docs_url"), None + ) + unexposed_endpoint._handle_registry_sync() + response = self.url_open( + self.endpoint_path(unexposed_endpoint, "/docs"), timeout=20 + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + @mute_logger("odoo.http") def assert_exception_processed( self, diff --git a/fastapi/views/fastapi_endpoint.xml b/fastapi/views/fastapi_endpoint.xml index 19df73918..cb13afcde 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -50,6 +50,7 @@ +