diff --git a/odoo/addons/test_http/controllers.py b/odoo/addons/test_http/controllers.py index a7dc5b9a5f8..1a2784b8c3f 100644 --- a/odoo/addons/test_http/controllers.py +++ b/odoo/addons/test_http/controllers.py @@ -3,8 +3,9 @@ import json import logging import werkzeug from odoo import http -from odoo.exceptions import UserError +from odoo.exceptions import AccessError, UserError from odoo.http import request +from odoo.tools import reraise_x_as from odoo.addons.web.controllers.utils import ensure_db @@ -148,3 +149,19 @@ class TestHttp(http.Controller): @http.route('/test_http/json_value_error', type='json', auth='none') def json_value_error(self): raise ValueError('Unknown destination') + + @http.route('/test_http/hide_errors/decorator', type='http', auth='none') + @reraise_x_as(AccessError, as_=werkzeug.exceptions.NotFound()) + def hide_errors_deco(self, error): + if error == 'AccessError': + raise AccessError("Wrong iris code") + if error == 'UserError': + raise UserError("Walter is AFK") + + @http.route('/test_http/hide_errors/context-manager', type='http', auth='none') + def hide_errors_cm(self, error): + with reraise_x_as(AccessError, as_=werkzeug.exceptions.NotFound()): + if error == 'AccessError': + raise AccessError("Wrong iris code") + if error == 'UserError': + raise UserError("Walter is AFK") diff --git a/odoo/addons/test_http/tests/__init__.py b/odoo/addons/test_http/tests/__init__.py index 451f37f515b..e0be1dc5bd2 100644 --- a/odoo/addons/test_http/tests/__init__.py +++ b/odoo/addons/test_http/tests/__init__.py @@ -1 +1,2 @@ +from . import test_error from . import test_http diff --git a/odoo/addons/test_http/tests/test_error.py b/odoo/addons/test_http/tests/test_error.py new file mode 100644 index 00000000000..b43971d48be --- /dev/null +++ b/odoo/addons/test_http/tests/test_error.py @@ -0,0 +1,26 @@ +from odoo.tools import mute_logger +from .test_http import TestHttpBase + + +class TestHttpErrorHttp(TestHttpBase): + @mute_logger('odoo.http') # UserError("Walter is AFK") + def test_httperror0_exceptions_as_404(self): + with self.subTest('Decorator/AccessError'): + res = self.nodb_url_open('/test_http/hide_errors/decorator?error=AccessError') + self.assertEqual(res.status_code, 404, "AccessError are configured to be hidden, they should be re-thrown as NotFound") + self.assertNotIn("Wrong iris code", res.text, "The real AccessError message should be hidden.") + + with self.subTest('Decorator/UserError'): + res = self.nodb_url_open('/test_http/hide_errors/decorator?error=UserError') + self.assertEqual(res.status_code, 400, "UserError are not configured to be hidden, they should be kept as-is.") + self.assertIn("Walter is AFK", res.text, "The real UserError message should be kept") + + with self.subTest('Context-Manager/AccessError'): + res = self.nodb_url_open('/test_http/hide_errors/context-manager?error=AccessError') + self.assertEqual(res.status_code, 404, "AccessError are configured to be hidden, they should be re-thrown as NotFound") + self.assertNotIn("Wrong iris code", res.text, "The real AccessError message should be hidden.") + + with self.subTest('Context-Manager/UserError'): + res = self.nodb_url_open('/test_http/hide_errors/context-manager?error=UserError') + self.assertEqual(res.status_code, 400, "UserError are not configured to be hidden, they should be kept as-is.") + self.assertIn("Walter is AFK", res.text, "The real UserError message should be kept") diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 93dab1664cf..e0868bd9741 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -25,7 +25,7 @@ import types import unicodedata from collections import OrderedDict from collections.abc import Iterable, Mapping, MutableMapping, MutableSet -from contextlib import contextmanager +from contextlib import ContextDecorator, contextmanager from difflib import HtmlDiff from functools import wraps from itertools import islice, groupby as itergroupby @@ -1261,6 +1261,47 @@ def ignore(*exc): except exc: pass +class replace_exceptions(ContextDecorator): + """ + Hide some exceptions behind another error. Can be used as a function + decorator or as a context manager. + + .. code-block: + + @route('/super/secret/route', auth='public') + @replace_exceptions(AccessError, by=NotFound()) + def super_secret_route(self): + if not request.session.uid: + raise AccessError("Route hidden to non logged-in users") + ... + + def some_util(): + ... + with replace_exceptions(ValueError, by=UserError("Invalid argument")): + ... + ... + + :param exceptions: the exception classes to catch and replace. + :param by: the exception to raise instead. + """ + def __init__(self, *exceptions, by): + if not exceptions: + raise ValueError("Missing exceptions") + + wrong_exc = next((exc for exc in exceptions if not issubclass(exc, Exception)), None) + if wrong_exc: + raise TypeError(f"{wrong_exc} is not an exception class.") + + self.exceptions = exceptions + self.by = by + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None and issubclass(exc_type, self.exceptions): + raise self.by from exc_value + html_escape = markupsafe.escape def get_lang(env, lang_code=False):