From 3252269baf3a260b3e5310d86d2fd271dd0fada6 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 6 Nov 2024 16:22:15 +0100 Subject: [PATCH] [ADD] fastapi_encrypted_errors --- extendable_fastapi/fastapi_dispatcher.py | 9 +- fastapi_encrypted_errors/README.rst | 99 ++++ fastapi_encrypted_errors/__init__.py | 3 + fastapi_encrypted_errors/__manifest__.py | 25 + .../fastapi_dispatcher.py | 32 ++ fastapi_encrypted_errors/models/__init__.py | 1 + .../models/fastapi_endpoint.py | 47 ++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 3 + fastapi_encrypted_errors/readme/USAGE.rst | 7 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 439 ++++++++++++++++++ fastapi_encrypted_errors/tests/__init__.py | 1 + .../tests/test_fastapi_encrypted_errors.py | 74 +++ .../views/fastapi_endpoint_views.xml | 31 ++ fastapi_encrypted_errors/wizards/__init__.py | 1 + .../wizards/wizard_fastapi_decrypt_errors.py | 40 ++ .../wizard_fastapi_decrypt_errors_views.xml | 47 ++ requirements.txt | 1 + .../odoo/addons/fastapi_encrypted_errors | 1 + setup/fastapi_encrypted_errors/setup.py | 6 + 21 files changed, 868 insertions(+), 4 deletions(-) create mode 100644 fastapi_encrypted_errors/README.rst create mode 100644 fastapi_encrypted_errors/__init__.py create mode 100644 fastapi_encrypted_errors/__manifest__.py create mode 100644 fastapi_encrypted_errors/fastapi_dispatcher.py create mode 100644 fastapi_encrypted_errors/models/__init__.py create mode 100644 fastapi_encrypted_errors/models/fastapi_endpoint.py create mode 100644 fastapi_encrypted_errors/readme/CONTRIBUTORS.rst create mode 100644 fastapi_encrypted_errors/readme/DESCRIPTION.rst create mode 100644 fastapi_encrypted_errors/readme/USAGE.rst create mode 100644 fastapi_encrypted_errors/security/ir.model.access.csv create mode 100644 fastapi_encrypted_errors/static/description/index.html create mode 100644 fastapi_encrypted_errors/tests/__init__.py create mode 100644 fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py create mode 100644 fastapi_encrypted_errors/views/fastapi_endpoint_views.xml create mode 100644 fastapi_encrypted_errors/wizards/__init__.py create mode 100644 fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py create mode 100644 fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml create mode 120000 setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors create mode 100644 setup/fastapi_encrypted_errors/setup.py diff --git a/extendable_fastapi/fastapi_dispatcher.py b/extendable_fastapi/fastapi_dispatcher.py index b940df9ce..7fe9d74c3 100644 --- a/extendable_fastapi/fastapi_dispatcher.py +++ b/extendable_fastapi/fastapi_dispatcher.py @@ -3,15 +3,16 @@ from contextlib import contextmanager +from odoo.http import _dispatchers + from odoo.addons.extendable.registry import _extendable_registries_database -from odoo.addons.fastapi.fastapi_dispatcher import ( - FastApiDispatcher as BaseFastApiDispatcher, -) from extendable import context -class FastApiDispatcher(BaseFastApiDispatcher): +# Inherit from last dispatcher of fastapi registered +# This handles multiple overloaded dispatchers +class FastApiDispatcher(_dispatchers["fastapi"]): routing_type = "fastapi" def dispatch(self, endpoint, args): diff --git a/fastapi_encrypted_errors/README.rst b/fastapi_encrypted_errors/README.rst new file mode 100644 index 000000000..7027f164e --- /dev/null +++ b/fastapi_encrypted_errors/README.rst @@ -0,0 +1,99 @@ +======================== +FastAPI Encrypted Errors +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:296fbef824a5eb64e9bbaedc382ef15e41d146f0cc59d458c0d76de46a54358e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_encrypted_errors + :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_encrypted_errors + :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| + +This module adds a "ref" field in the error response of FastAPI. +This field is an AES encrypted string that contains the error message / traceback. +This encrypted string can be decrypted using the endpoint decrypt error wizard. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +First you have to enable the encryption for an endpoint by checking the `Encrypt Errors` checkbox +in the endpoint configuration. + +To decrypt an error message, you can use the "Decrypt Error" wizard in the +FastAPI menu. + +You can regenerate a new key by clicking on the "Regenerate Key" button next to the `Errors Secret Key` field. + +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 +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + + * Florian Mounier + +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-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +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_encrypted_errors/__init__.py b/fastapi_encrypted_errors/__init__.py new file mode 100644 index 000000000..d7f65dfa1 --- /dev/null +++ b/fastapi_encrypted_errors/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import wizards +from . import fastapi_dispatcher diff --git a/fastapi_encrypted_errors/__manifest__.py b/fastapi_encrypted_errors/__manifest__.py new file mode 100644 index 000000000..254901404 --- /dev/null +++ b/fastapi_encrypted_errors/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "FastAPI Encrypted Errors", + "summary": "Adds encrypted error messages to FastAPI error responses.", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "maintainers": ["paradoxxxzero"], + "website": "https://github.com/OCA/rest-framework", + "depends": [ + "fastapi", + ], + "data": [ + "security/ir.model.access.csv", + "views/fastapi_endpoint_views.xml", + "wizards/wizard_fastapi_decrypt_errors_views.xml", + ], + "demo": [], + "external_dependencies": { + "python": ["cryptography"], + }, +} diff --git a/fastapi_encrypted_errors/fastapi_dispatcher.py b/fastapi_encrypted_errors/fastapi_dispatcher.py new file mode 100644 index 000000000..24a14b928 --- /dev/null +++ b/fastapi_encrypted_errors/fastapi_dispatcher.py @@ -0,0 +1,32 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.http import _dispatchers + +from odoo.addons.fastapi.error_handlers import convert_exception_to_status_body + + +# Inherit from last dispatcher of fastapi registered +# This handles multiple overloaded dispatchers +class FastApiDispatcher(_dispatchers["fastapi"]): + routing_type = "fastapi" + + def handle_error(self, exc): + environ = self._get_environ() + root_path = "/" + environ["PATH_INFO"].split("/")[1] + fastapi_endpoint = ( + self.request.env["fastapi.endpoint"] + .sudo() + .search([("root_path", "=", root_path)]) + ) + if fastapi_endpoint.encrypt_errors: + headers = getattr(exc, "headers", None) + status_code, body = convert_exception_to_status_body(exc) + if body: + body["ref"] = fastapi_endpoint._encrypt_error(exc) + return self.request.make_json_response( + body, status=status_code, headers=headers + ) + + return super().handle_error(exc) diff --git a/fastapi_encrypted_errors/models/__init__.py b/fastapi_encrypted_errors/models/__init__.py new file mode 100644 index 000000000..b825fab92 --- /dev/null +++ b/fastapi_encrypted_errors/models/__init__.py @@ -0,0 +1 @@ +from . import fastapi_endpoint diff --git a/fastapi_encrypted_errors/models/fastapi_endpoint.py b/fastapi_encrypted_errors/models/fastapi_endpoint.py new file mode 100644 index 000000000..add3ca2ec --- /dev/null +++ b/fastapi_encrypted_errors/models/fastapi_endpoint.py @@ -0,0 +1,47 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import traceback +import zlib + +from cryptography.fernet import Fernet + +from odoo import fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + encrypt_errors = fields.Boolean( + help="Encrypt errors before sending them to the client.", + ) + encrypted_errors_secret_key = fields.Char( + help="The secret key used to encrypt errors before sending them to the client.", + default=lambda _: Fernet.generate_key(), + readonly=True, + ) + + def action_generate_encrypted_errors_secret_key(self): + for record in self: + record.encrypted_errors_secret_key = Fernet.generate_key() + + def _encrypt_error(self, exc): + self.ensure_one() + if not self.encrypt_errors or not self.encrypted_errors_secret_key: + return + + # Get full traceback + error = "".join(traceback.format_exception(exc)) + # zlib compression works quite well on tracebacks + error = zlib.compress(error.encode("utf-8")) + f = Fernet(self.encrypted_errors_secret_key) + return f.encrypt(error) + + def _decrypt_error(self, error): + self.ensure_one() + if not self.encrypt_errors or not self.encrypted_errors_secret_key: + return + + f = Fernet(self.encrypted_errors_secret_key) + error = f.decrypt(error) + return zlib.decompress(error).decode("utf-8") diff --git a/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst b/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..a4d0ad922 --- /dev/null +++ b/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + + * Florian Mounier diff --git a/fastapi_encrypted_errors/readme/DESCRIPTION.rst b/fastapi_encrypted_errors/readme/DESCRIPTION.rst new file mode 100644 index 000000000..3ea245c61 --- /dev/null +++ b/fastapi_encrypted_errors/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module adds a "ref" field in the error response of FastAPI. +This field is an AES encrypted string that contains the error message / traceback. +This encrypted string can be decrypted using the endpoint decrypt error wizard. diff --git a/fastapi_encrypted_errors/readme/USAGE.rst b/fastapi_encrypted_errors/readme/USAGE.rst new file mode 100644 index 000000000..041077d9b --- /dev/null +++ b/fastapi_encrypted_errors/readme/USAGE.rst @@ -0,0 +1,7 @@ +First you have to enable the encryption for an endpoint by checking the `Encrypt Errors` checkbox +in the endpoint configuration. + +To decrypt an error message, you can use the "Decrypt Error" wizard in the +FastAPI menu. + +You can regenerate a new key by clicking on the "Regenerate Key" button next to the `Errors Secret Key` field. diff --git a/fastapi_encrypted_errors/security/ir.model.access.csv b/fastapi_encrypted_errors/security/ir.model.access.csv new file mode 100644 index 000000000..d102f0c82 --- /dev/null +++ b/fastapi_encrypted_errors/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fastapi_wizard_auth_partner_impersonate,wizard_fastapi_decrypt_errors,model_wizard_fastapi_decrypt_errors,fastapi.group_fastapi_manager,1,1,1,1 diff --git a/fastapi_encrypted_errors/static/description/index.html b/fastapi_encrypted_errors/static/description/index.html new file mode 100644 index 000000000..0ffce082e --- /dev/null +++ b/fastapi_encrypted_errors/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +FastAPI Encrypted Errors + + + +
+

FastAPI Encrypted Errors

+ + +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module adds a “ref” field in the error response of FastAPI. +This field is an AES encrypted string that contains the error message / traceback. +This encrypted string can be decrypted using the endpoint decrypt error wizard.

+

Table of contents

+ +
+

Usage

+

First you have to enable the encryption for an endpoint by checking the Encrypt Errors checkbox +in the endpoint configuration.

+

To decrypt an error message, you can use the “Decrypt Error” wizard in the +FastAPI menu.

+

You can regenerate a new key by clicking on the “Regenerate Key” button next to the Errors Secret Key field.

+
+
+

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

+
    +
  • Akretion
  • +
+
+
+

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:

+

paradoxxxzero

+

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_encrypted_errors/tests/__init__.py b/fastapi_encrypted_errors/tests/__init__.py new file mode 100644 index 000000000..8fe3d60a6 --- /dev/null +++ b/fastapi_encrypted_errors/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_encrypted_errors diff --git a/fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py b/fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py new file mode 100644 index 000000000..8aa1174f7 --- /dev/null +++ b/fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py @@ -0,0 +1,74 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import os +import unittest + +from odoo.tests.common import HttpCase + +from odoo.addons.fastapi.schemas import DemoExceptionType + +from fastapi import status + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") +class FastAPIEncryptedErrorsCase(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.write({"encrypt_errors": True}) + lang = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "fr_BE")]) + ) + lang.active = True + + def test_encrypted_errors_in_response(self): + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=User Error" + ) + response = self.url_open(route, timeout=200) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + res = response.json() + self.assertEqual(res["detail"], "User Error") + self.assertIn("ref", res) + + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.bare_exception.value}" + "&error_message=Internal Server Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + res = response.json() + self.assertEqual(res["detail"], "Internal Server Error") + self.assertIn("ref", res) + + def test_encrypted_errors_decrypt(self): + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.bare_exception.value}" + "&error_message=Internal Server Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + res = response.json() + self.assertEqual(res["detail"], "Internal Server Error") + self.assertIn("ref", res) + ref = res["ref"] + self.assertNotIn("Traceback (most recent call last)", ref) + self.assertNotIn("NotImplementedError: Internal Server Error", ref) + + wizard = self.env["wizard.fastapi.decrypt.errors"].create({"error": ref}) + wizard.action_decrypt_error() + self.assertIn("Traceback (most recent call last)", wizard.decrypted_error) + self.assertIn( + "NotImplementedError: Internal Server Error", wizard.decrypted_error + ) diff --git a/fastapi_encrypted_errors/views/fastapi_endpoint_views.xml b/fastapi_encrypted_errors/views/fastapi_endpoint_views.xml new file mode 100644 index 000000000..fce00f36f --- /dev/null +++ b/fastapi_encrypted_errors/views/fastapi_endpoint_views.xml @@ -0,0 +1,31 @@ + + + + + + fastapi.endpoint + + + + +
+
+
+
+
+
diff --git a/fastapi_encrypted_errors/wizards/__init__.py b/fastapi_encrypted_errors/wizards/__init__.py new file mode 100644 index 000000000..a160dabbb --- /dev/null +++ b/fastapi_encrypted_errors/wizards/__init__.py @@ -0,0 +1 @@ +from . import wizard_fastapi_decrypt_errors diff --git a/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py new file mode 100644 index 000000000..d19bd6d3a --- /dev/null +++ b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py @@ -0,0 +1,40 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import traceback + +from odoo import fields, models + + +class WizardFastapiDecryptErrors(models.TransientModel): + _name = "wizard.fastapi.decrypt.errors" + _description = "Wizard to decrypt FastAPI errors" + + error = fields.Text(required=True) + fastapi_endpoint_id = fields.Many2one( + "fastapi.endpoint", + string="FastAPI Endpoint", + required=True, + default=lambda self: self.env["fastapi.endpoint"].search([], limit=1), + ) + decrypted_error = fields.Text(readonly=True) + + def action_decrypt_error(self): + self.ensure_one() + try: + error = self.fastapi_endpoint_id._decrypt_error(self.error.encode("utf-8")) + except Exception: + self.decrypted_error = ( + "Error while decrypting error: \n\n" + traceback.format_exc() + ) + else: + self.decrypted_error = error + + return { + "type": "ir.actions.act_window", + "res_model": "wizard.fastapi.decrypt.errors", + "view_mode": "form", + "view_type": "form", + "res_id": self.id, + "target": "new", + } diff --git a/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml new file mode 100644 index 000000000..62b8a803d --- /dev/null +++ b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml @@ -0,0 +1,47 @@ + + + + + wizard.fastapi.decrypt.errors + +
+ + + + + +
+
+ +
+
+
+ + + Decrypt Error + wizard.fastapi.decrypt.errors + ir.actions.act_window + form + new + + + + + +
diff --git a/requirements.txt b/requirements.txt index 7e0b84839..fe14d9447 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ apispec apispec>=4.0.0 cerberus contextvars +cryptography extendable-pydantic extendable-pydantic>=1.2.0 extendable>=0.0.4 diff --git a/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors b/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors new file mode 120000 index 000000000..101a9234a --- /dev/null +++ b/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors @@ -0,0 +1 @@ +../../../../fastapi_encrypted_errors \ No newline at end of file diff --git a/setup/fastapi_encrypted_errors/setup.py b/setup/fastapi_encrypted_errors/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_encrypted_errors/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)