[IMP] core: utility to reraise errors as others

We introduce a new decorator/context-manager to catch some exceptions
and re-raise them as another error. This utility main's purpose is to
hide a route that a user has no access to behind a fake HTTP 404 Page
not Found error.

The utility is at `odoo.tools.misc.replace_exceptions(*exceptions, by)`.

Its usage is as follow:

    @route('/some/route', auth='public')
    @replace_exceptions(AccessError, AccessDenied, by=NotFound())
    def some_route(self):
        if not request.session.uid:
            raise AccessError("Must be connected to see this route")
        ...

Or as a context-manager if you don't want to except an entire function:

    @route('/some/route', auth='public')
    def some_route(self):
        with replace_exceptions(AccessError, AccessDenied, by=NotFound()):
            if not request.session.uid:
                raise AccessError("Must be connected to see this route")
        ...

Task: 2800772
Close: #90433
Part-of: odoo/odoo#88134
This commit is contained in:
Julien Castiaux
2022-06-01 02:53:59 +02:00
parent 18e9076a8f
commit 49efab6958
4 changed files with 87 additions and 2 deletions
+18 -1
View File
@@ -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")
+1
View File
@@ -1 +1,2 @@
from . import test_error
from . import test_http
+26
View File
@@ -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")
+42 -1
View File
@@ -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):