[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:
@@ -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 +1,2 @@
|
||||
from . import test_error
|
||||
from . import test_http
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user