Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle_params_access_error not called during exception in controllers #162752

Open
FandyTADA opened this issue Apr 20, 2024 · 1 comment
Open

Comments

@FandyTADA
Copy link

FandyTADA commented Apr 20, 2024

Impacted versions: 17

Steps to reproduce:
Create a new controllers.py to return JSON response

from odoo import http

import odoo
import logging

def handle_error(exception):
    _logger.info('Handle Unauthorized Error')
    _logger.info(exception)

    if isinstance(exception, odoo.exceptions.AccessError):
        body = json.dumps({
            'jsonrpc': '2.0',
            'id': None,
            'result': {'error': {'code': 'unauthorized', 'message:': 'Unauthorized'}}}, default=date_utils.json_default)

        mime = 'application/json'

        return Response(body, mimetype=mime, headers=[('Content-Type', mime), ('Content-Length', len(body))], status=401)

class AuthController(http.Controller):

    @http.route('/api/v1/accounts/login',
                type='json',
                methods=['POST'],
                auth='public',
                csrf=False,
                handle_params_access_error=handle_error)
    def login(self):
        raise odoo.exceptions.AccessError('ERROR')

Call the API using curl:

curl --location 'http://localhost:10017/api/v1/accounts/login' \
--header 'Content-Type: application/json' \
--header 'Cookie: frontend_lang=en_US; session_id=a12c9d3859114bf495054ca05abd2b85aee2b0c0' \
--data '{}'

Current behavior:

It looks like the handle_params_access_error is not called, my _logger.info('Handle Unauthorized Error') is not printed in console

{
    "jsonrpc": "2.0",
    "id": null,
    "error": {
        "code": 200,
        "message": "Odoo Server Error",
        "data": {
            "name": "odoo.exceptions.AccessError",
            "debug": "Traceback (most recent call last):\n  File \"/usr/lib/python3/dist-packages/odoo/http.py\", line 1765, in _serve_db\n    return service_model.retrying(self._serve_ir_http, self.env)\n  File \"/usr/lib/python3/dist-packages/odoo/service/model.py\", line 133, in retrying\n    result = func()\n  File \"/usr/lib/python3/dist-packages/odoo/http.py\", line 1792, in _serve_ir_http\n    response = self.dispatcher.dispatch(rule.endpoint, args)\n  File \"/usr/lib/python3/dist-packages/odoo/http.py\", line 1996, in dispatch\n    result = self.request.registry['ir.http']._dispatch(endpoint)\n  File \"/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_http.py\", line 222, in _dispatch\n    result = endpoint(**request.params)\n  File \"/usr/lib/python3/dist-packages/odoo/http.py\", line 722, in route_wrapper\n    result = endpoint(self, *args, **params_ok)\n  File \"/mnt/extra-addons/hr_gramedia_attendance/controllers/auth_controllers.py\", line 33, in login\n    raise odoo.exceptions.AccessError('ERROR')\nodoo.exceptions.AccessError: ERROR\n",
            "message": "ERROR",
            "arguments": [
                "ERROR"
            ],
            "context": {}
        }
    }
}

Expected behavior:
JSON Response return 401 with following result:

{
  'jsonrpc': '2.0',
  'id': None,
  'result': {
    'error': {
      'code': 'unauthorized',
      'message:': 'Unauthorized'
    }
  }
}

I've checked the source code by looking for handle_params_access_error, and found that in ir_http.py the parameter is called

try:
                # explicitly crash now, instead of crashing later
                args[key].check_access_rights('read')
                args[key].check_access_rule('read')
            except (odoo.exceptions.AccessError, odoo.exceptions.MissingError) as e:
                # custom behavior in case a record is not accessible / has been removed
                if handle_error := rule.endpoint.original_routing.get('handle_params_access_error'):
                    if response := handle_error(e):
                        werkzeug.exceptions.abort(response)
                if isinstance(e, odoo.exceptions.MissingError):
                    raise werkzeug.exceptions.NotFound() from e
                raise

i could not found any other code to handle this parameter. please advise.

@FandyTADA
Copy link
Author

FandyTADA commented Apr 21, 2024

I did some hack to be able to return other than 200 in JsonRPCDispatcher

  1. First i create custom exceptions
class JsonRPCException(Exception):
    pass

class UnauthorizedException(JsonRPCException):
    pass
  1. Then in my addons module __init__.py i override JsonRPCDispatcher dispatcher method to handle handle_params_access_error parameter
from . import models
from . import controllers

from odoo.http import Dispatcher, Response, JsonRPCDispatcher, serialize_exception, NotFound, SessionExpiredException
from werkzeug.exceptions import abort

import logging
import werkzeug
import collections

_logger = logging.getLogger(__name__)

JSON_MIMETYPES = ('application/json', 'application/json-rpc')

class JsonRPCDispatcherPatch(Dispatcher):
    routing_type = 'json'

    @classmethod
    def is_compatible_with(cls, request):
        return request.httprequest.mimetype in JSON_MIMETYPES

    def handle_error(self, exc: Exception) -> collections.abc.Callable:
        error = {
            'code': 200,  # this code is the JSON-RPC level code, it is
            # distinct from the HTTP status code. This
            # code is ignored and the value 200 (while
            # misleading) is totally arbitrary.
            'message': "Odoo Server Error",
            'data': serialize_exception(exc),
        }
        if isinstance(exc, NotFound):
            error['code'] = 404
            error['message'] = "404: Not Found"
        elif isinstance(exc, SessionExpiredException):
            error['code'] = 100
            error['message'] = "Odoo Session Expired"

        return self._response(error=error)

    def dispatch(self, endpoint, args):
        try:
            self.jsonrequest = self.request.get_json_data()
            self.request_id = self.jsonrequest.get('id')
        except ValueError as exc:
            # must use abort+Response to bypass handle_error
            werkzeug.exceptions.abort(Response("Invalid JSON data", status=400))
        except AttributeError as exc:
            # must use abort+Response to bypass handle_error
            werkzeug.exceptions.abort(Response("Invalid JSON-RPC data", status=400))

        self.request.params = dict(self.jsonrequest.get('params', {}), **args)

        try:
            if self.request.db:
                result = self.request.registry['ir.http']._dispatch(endpoint)
            else:
                result = endpoint(**self.request.params)

            return self._response(result)
        except JsonRPCException as exc:
            if handle_error := endpoint.original_routing.get('handle_params_access_error'):
                if response := handle_error(exc):
                    werkzeug.exceptions.abort(response)

    def _response(self, result=None, error=None):
        response = {'jsonrpc': '2.0', 'id': self.request_id}
        if error is not None:
            response['error'] = error
        if result is not None:
            response['result'] = result

        return self.request.make_json_response(response)

JsonRPCDispatcher.dispatch = JsonRPCDispatcherPatch.dispatch
  1. Then in your controller.py you can raise the custom exception, e.g:
from odoo import http
import logging

_logger = logging.getLogger(__name__)

def handle_error(exception):
    if isinstance(exception, UnauthorizedException):
        body = json.dumps({
            'jsonrpc': '2.0',
            'id': None,
            'result': {'error': {'code': 'unauthorized', 'message:': 'Unauthorized'}}}, default=date_utils.json_default)

        mime = 'application/json'

        return Response(body, mimetype=mime, headers=[('Content-Type', mime), ('Content-Length', len(body))], status=401)

class AuthController(http.Controller):

    @http.route('/api/v1/accounts/login',
                type='json',
                methods=['POST'],
                auth='public',
                csrf=False,
                handle_params_access_error=handle_error)
    def login(self):
        raise UnauthorizedException()
  1. If you hit the endpoint, you will received 401 Unauthorized HTTP Status instead of 200 with the response:
{
    "jsonrpc": "2.0",
    "id": null,
    "result": {
        "error": {
            "code": "unauthorized",
            "message:": "Unauthorized"
        }
    }
}

hope this help as temp solution for anyone with the same issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant