If using decorators roles_accepted or roles_required and user don't have the role that is needed - he will be redirected from domain.com/tester to domain.com/ , when it is expected that unauthorized_handler should be called. Decorator login_required works fine. What is the problem? Maybe for roles I need to define another callback?
@auth_bp.route("/tester")
@decorators.login_required
@decorators.roles_accepted('tester')
def tester():
return "Hello, Tester"
@app.login_manager.unauthorized_handler
def unauthorized_callback():
return unauthorized_response
The problem is that app.login_manager is part of Flask-Login, but Flask-Security uses its own separate unauthorized handler attribute.
Depending on whether or not you're using the application factory pattern, what you need to do looks like this:
security = Security(app, datastore)
security._state.unauthorized_handler(your_unauthorized_callback_fn)
# or when using an app factory
security = Security()
security_state = security.init_app(app, datastore)
security_state.unauthorized_handler(your_unauthorized_callback_fn)
Yeah, your solution works :+1: Thanks! Do I still need @app.login_manager.unauthorized_handler decorator after setting unauthorized_handler explicitly?
Only if you want to continue using the login_required decorator (which itself is part of Flask-Login). Using auth_required('session') from Flask-Security is the equivalent that would use the unauthorized_handler declared on the Security extension state.
Personally, I don't find the singular unauthorized_handler granular enough, so in my projects I actually reimplement the roles decorators so that they use abort(403) while the unauthorized_handler uses abort(401) (because unauthorized means "i don't know who you are; please log in" while forbidden means "i know who you are, and you don't have permission")
The complete solution I use looks like this:
from flask import abort
from flask_principal import Permission, RoleNeed
from functools import wraps
from http import HTTPStatus
def roles_accepted(*roles):
"""Decorator which specifies that a user must have at least one of the
specified roles.
Aborts with HTTP: 403 if the user doesn't have at least one of the roles
Example::
@app.route('/create_post')
@roles_accepted('ROLE_ADMIN', 'ROLE_EDITOR')
def create_post():
return 'Create Post'
The current user must have either the `ROLE_ADMIN` role or `ROLE_EDITOR`
role in order to view the page.
:param roles: The possible roles.
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
perm = Permission(*[RoleNeed(role) for role in roles])
if not perm.can():
abort(HTTPStatus.FORBIDDEN)
return fn(*args, **kwargs)
return decorated_view
return wrapper
from flask import abort
from flask_principal import Permission, RoleNeed
from functools import wraps
from http import HTTPStatus
def roles_required(*roles):
"""Decorator which specifies that a user must have all the specified roles.
Aborts with HTTP 403: Forbidden if the user doesn't have the required roles
Example::
@app.route('/dashboard')
@roles_required('ROLE_ADMIN', 'ROLE_EDITOR')
def dashboard():
return 'Dashboard'
The current user must have both the `ROLE_ADMIN` and `ROLE_EDITOR` roles
in order to view the page.
:param roles: The required roles.
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
perms = [Permission(RoleNeed(role)) for role in roles]
for perm in perms:
if not perm.can():
abort(HTTPStatus.FORBIDDEN)
return fn(*args, **kwargs)
return decorated_view
return wrapper
from flask_security.decorators import auth_required as security_auth_required
from functools import wraps
from .roles_accepted import roles_accepted
from .roles_required import roles_required
def auth_required(*decorator_args, **decorator_kwargs):
"""Decorator for requiring an authenticated user, optionally with roles
Roles are passed as keyword arguments, like so:
@auth_required(role='REQUIRE_THIS_ONE_ROLE')
@auth_required(roles=['REQUIRE', 'ALL', 'OF', 'THESE', 'ROLES'])
@auth_required(one_of=['EITHER_THIS_ROLE', 'OR_THIS_ONE'])
One of role or roles kwargs can also be combined with one_of:
@auth_required(role='REQUIRED', one_of=['THIS', 'OR_THIS'])
Aborts with HTTP 401: Unauthorized if no user is logged in, or
HTTP 403: Forbidden if any of the specified role checks fail
"""
required_roles = []
one_of_roles = []
if not (decorator_args and callable(decorator_args[0])):
if 'role' in decorator_kwargs and 'roles' in decorator_kwargs:
raise RuntimeError('specify only one of `role` or `roles` kwargs')
elif 'role' in decorator_kwargs:
required_roles = [decorator_kwargs['role']]
elif 'roles' in decorator_kwargs:
required_roles = decorator_kwargs['roles']
if 'one_of' in decorator_kwargs:
one_of_roles = decorator_kwargs['one_of']
def wrapper(fn):
@wraps(fn)
@security_auth_required('session', 'token')
@roles_required(*required_roles)
@roles_accepted(*one_of_roles)
def decorated(*args, **kwargs):
return fn(*args, **kwargs)
return decorated
# allow using the decorator without parenthesis
if decorator_args and callable(decorator_args[0]):
return wrapper(decorator_args[0])
return wrapper
@briancappello
the 403 Forbiden is proper, but with a forbiden_handler(self, fun) is better
Most helpful comment
Only if you want to continue using the
login_requireddecorator (which itself is part of Flask-Login). Usingauth_required('session')from Flask-Security is the equivalent that would use the unauthorized_handler declared on the Security extension state.Personally, I don't find the singular unauthorized_handler granular enough, so in my projects I actually reimplement the roles decorators so that they use
abort(403)while the unauthorized_handler usesabort(401)(because unauthorized means "i don't know who you are; please log in" while forbidden means "i know who you are, and you don't have permission")The complete solution I use looks like this:
roles_accepted (aka one of these roles)
roles_required (aka all of these roles)
auth_required (in practice, this is the decorator i use 95% of the time, to check both authentication and authorization)