arxiv.users.auth.decorators module

Scope-based authorization of user/client requests.

This module provides scoped(), a decorator factory used to protect Flask routes for which authorization is required. This is done by specifying a required authorization scope (see arxiv.users.auth.scopes) and/or by providing a custom authorizer function.

For routes that involve specific resources, a resource callback should also be provided. That callback function should accept the same arguments as the route function, and return the identifier for the resource as a string.

Using scoped() with an authorizer function allows you to define application-specific authorization logic on a per-request basis without adding complexity to request controllers. The call signature of the authorizer function should be: (session: domain.Session, *args, **kwargs) -> bool, where *args and **kwargs are the positional and keyword arguments, respectively, passed by Flask to the decorated route function (e.g. the URL parameters).

Note

The authorizer function is only called if the session does not have a global or resource-specific instance of the required scope, or if a required scope is not specified.

Here’s an example of how you might use this in a Flask application:

from arxiv.users.auth.decorators import scoped
from arxiv.users.auth import scopes
from arxiv.users import domain


def is_owner(session: domain.Session, user_id: str, **kwargs) -> bool:
    '''Check whether the authenticated user matches the requested user.'''
    return session.user.user_id == user_id


def get_resource_id(user_id: str) -> str:
    '''Get the user ID from the request.'''
    return user_id


def redirect_to_login(user_id: str) -> Response:
    '''Send the unauthorized user to the log in page.'''
    return url_for('login')


@blueprint.route('/<string:user_id>/profile', methods=['GET'])
@scoped(scopes.EDIT_PROFILE, resource=get_resource_id,
        authorizer=user_is_owner, unauthorized=redirect_to_login)
def edit_profile(user_id: str):
    '''User can update their account information.'''
    data, code, headers = profile.get_profile(user_id)
    return render_template('accounts/profile.html', **data)

When the decorated route function is called…

  • If no session is available from either the middleware or the legacy database, the unauthorized callback is called, and/or Unauthorized exception is raised.
  • If a required scope was provided, the session is checked for the presence of that scope in this order:
    • Global scope (:*), e.g. for administrators.
    • Resource-specific scope (:[resource_id]), i.e. explicitly granted for a particular resource.
    • Generic scope (no resource part).
  • If an authorization function was provided, the function is called only if a required scope was not provided, or if only the generic scope was found.
  • Session data is added directly to the Flask request object as request.session, for ease of access elsewhere in the application.
  • Finally, if no exceptions have been raised, the route is called with the original parameters.
arxiv.users.auth.decorators.scoped(required=None, resource=None, authorizer=None, unauthorized=None)[source]

Generate a decorator to enforce authorization requirements.

Parameters:
  • required (str) – The scope required on a user or client session in order use the decorated route. See arxiv.users.auth.scopes. If not provided, no scope will be enforced.
  • resource (function) – If a route provides actions for a specific resource, a callable should be provided that accepts the route arguments and returns the resource identifier (str).
  • authorizer (function) – In addition, an authorizer function may be passed to provide more specific authorization checks. For example, this function may check that the requesting user is the owner of a resource. Should have the signature: (session: domain.Session, *args, **kwargs) -> bool. *args and **kwargs are the parameters passed to the decorated function. If the authorizer returns False, an exception is raised.
  • unauthorized (function) – A callback may be passed to handle cases in which the request is unauthorized. This function will be passed the same arguments as the original route function. If the callback returns anything other than None, the return value will be treated as a response and execution will stop. Otherwise, an Unauthorized exception will be raised. If a callback is not provided (default) an Unauthorized exception will be raised.
Returns:

A decorator that enforces the required scope and calls the (optionally) provided authorizer.

Return type:

function

Return type:

Callable