pallets-eco/flask-principal

Struggling with using `http_exception=`

Closed this issue · 4 comments

I'm starting to use Flask-Principal for adding granular resource protection to a REST API written using Flask-RESTful/Flask-RestPlus. For almost all of my permission checks I just want to abort with a 403 error when presented with insufficient permissions so I'm trying to use the decorators like so:

admin_permission = Permission(RoleNeed('admin'))

@app.route('/foo')
class Foo(Resource):
    @login_required
    @admin_permission.require(http_exception=403)
    def get(self):
        pass

However if I trigger a permission denied error, I end up getting:

Traceback (most recent call last):
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__
    return self.wsgi_app(environ, start_response)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File ".../venv/lib/python2.7/site-packages/flask_restful/__init__.py", line 270, in error_router
    return original_handler(e)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
  File ".../venv/lib/python2.7/site-packages/flask_restful/__init__.py", line 267, in error_router
    return self.handle_error(e)
  File ".../venv/lib/python2.7/site-packages/flask_restplus/api.py", line 379, in handle_error
    return super(Api, self).handle_error(e)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File ".../venv/lib/python2.7/site-packages/flask_restful/__init__.py", line 270, in error_router
    return original_handler(e)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1363, in handle_user_exception
    assert exc_value is e
AssertionError

If I don't use http_exception= then I get PermissionDenied exceptions with a 500 error code which aren't terribly useful. The only way I can seem to get things working is with:

admin_permission = Permission(RoleNeed('admin'))

@app.route('/foo')
class Foo(Resource):
    @login_required
    def get(self):
        if admin_permission.can():
            pass
        else:
            abort(403)

I'm concerned I'm going to end up writing the same block of code everywhere so I'd like to make use of http_exception= if possible. Is it possibly because I'm using Flask-RESTful/Flask-RestPlus which tries to JSON-ify responses perhaps?

I believe this is a regression in Flask-restful 3.3.4. As http_exception=403 is working correctly in 3.3.3. Weirdly though it is only affecting 2.7x not the 3.3x or 3.4x versions of python.

Yes, I've just tested a few combinations. If I downgrade to Flask-RESTful 0.3.3 then everything seems to work as expected, I get a JSON-formatted error response:

{
    "message": "Forbidden",
    "status": 403
}

If I upgrade to the latest version again (0.3.4) then yes, it breaks as I wrote for Python 2.7.10, and if I try Python 3.5.0 then it works, but I get an HTML-formatted error response like the below:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>403 Forbidden</title>
<h1>Forbidden</h1>
<p>&lt;Permission needs={Need(method='role', value='admin')} excludes=set()&gt;</p>

@bodgit : For anyone looking for a solution to the HTML output, you need to set a error handler:

    @app.errorhandler(403)
    def handle_error(e):
        return jsonify(error='Forbidden'), 403

I know this is old but I was just having trouble with this as well and as well as the above errorhandler(403) you can pass an exception to the error handler and have:

    @app.errorhandler(PermissionDenied)
    def handle_error(e):
        return jsonify(error='Forbidden'), 403

or in my case with Flask-RESTPlus @api.errorhandler(PermissionDenied) to just target the flask-principal rendering.