Skip to content
This repository has been archived by the owner on Jul 30, 2024. It is now read-only.

roles_accepted and roles_required redirects instead of calling unauthorized_handler #769

Closed
HarrySky opened this issue Mar 29, 2018 · 4 comments

Comments

@HarrySky
Copy link

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
@briancappello
Copy link
Contributor

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)

@HarrySky
Copy link
Author

HarrySky commented Apr 4, 2018

Yeah, your solution works 👍 Thanks! Do I still need @app.login_manager.unauthorized_handler decorator after setting unauthorized_handler explicitly?

@HarrySky HarrySky closed this as completed Apr 4, 2018
@briancappello
Copy link
Contributor

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:

roles_accepted (aka one of these roles)

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

roles_required (aka all of these roles)

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

auth_required (in practice, this is the decorator i use 95% of the time, to check both authentication and authorization)

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

@micfan
Copy link

micfan commented Nov 2, 2018

@briancappello

the 403 Forbiden is proper, but with a forbiden_handler(self, fun) is better

jasco pushed a commit to jasco/flask-security that referenced this issue Oct 3, 2023
The `user` arg was removed from `MailUtil.send_mail` in pallets-eco#690.

Co-authored-by: Chris Wagner <jwag.wagner@gmail.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

No branches or pull requests

3 participants