diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 660bb2c5d..a907ceadc 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -65,6 +65,7 @@ class FastapiEndpoint(models.Model): "unexpecteed disk space consumption.", default=True, ) + expose_doc_urls = fields.Boolean("Expose FastAPI docs", default=True) @api.depends("root_path") def _compute_root_path(self): @@ -150,7 +151,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""" @@ -321,12 +322,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 35fa7ac99..44c252aa6 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 import sql_db from odoo.tests.common import HttpCase @@ -45,6 +46,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) @@ -79,6 +91,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 77b04fd9b..af516a0b0 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -49,6 +49,7 @@ +