Source code for coaster.auth

"""
Authentication management
=========================

Coaster provides a :obj:`current_auth` for handling authentication. Login
managers must comply with its API for Coaster's view handlers to work.

If a login manager installs itself as ``current_app.login_manager`` and
provides a ``_load_user()`` method, it will be called when :obj:`current_auth`
is invoked for the first time in a request. Login managers can call
:func:`add_auth_attribute` to load the actor (typically the authenticated user)
and any other relevant authentication attributes.

For compatibility with Flask-Login, a user object loaded at
``_request_ctx_stack.top.user`` will be recognised and made available via
:obj:`current_auth`.
"""

# mypy can't find _request_ctx_stack in flask
from flask import (  # type: ignore[attr-defined]
    _request_ctx_stack,
    current_app,
    has_request_context,
)
from werkzeug.local import LocalProxy

from .utils import InspectableSet

__all__ = ['add_auth_attribute', 'add_auth_anchor', 'request_has_auth', 'current_auth']


[docs]def add_auth_attribute(attr, value, actor=False): """ Helper function for login managers. Adds authorization attributes to :obj:`current_auth` for the duration of the request. :param str attr: Name of the attribute :param value: Value of the attribute :param bool actor: Whether this attribute is an actor (user or client app accessing own data) If the attribute is an actor and :obj:`current_auth` does not currently have an actor, the attribute is also made available as ``current_auth.actor``, which in turn is used by ``current_auth.is_authenticated``. The attribute name ``user`` is special-cased: 1. ``user`` is always treated as an actor 2. ``user`` is also made available as ``_request_ctx_stack.top.user`` for compatibility with Flask-Login """ if attr in ( 'actor', 'anchors', 'is_anonymous', 'not_anonymous', 'is_authenticated', 'not_authenticated', ): raise AttributeError("Attribute name %s is reserved by current_auth" % attr) # Invoking current_auth will also create it on the local stack. We can # then proceed to set attributes on it. ca = current_auth._get_current_object() # Since :class:`CurrentAuth` overrides ``__setattr__``, we need to use # :class:`object`'s. object.__setattr__(ca, attr, value) if attr == 'user': # Special-case 'user' for compatibility with Flask-Login _request_ctx_stack.top.user = value # A user is always an actor actor = True if actor: object.__setattr__(ca, 'actor', value)
[docs]def add_auth_anchor(anchor): """ Helper function for login managers and view handlers to add a new auth anchor. This is a placeholder until anchors are properly specified. """ existing = set(current_auth.anchors) existing.add(anchor) ca = current_auth._get_current_object() object.__setattr__(ca, 'anchors', frozenset(existing))
[docs]def request_has_auth(): """ Helper function that returns True if :obj:`current_auth` was invoked during the current request. A login manager can use this during request teardown to set cookies or perform other housekeeping functions. """ return hasattr(_request_ctx_stack.top, 'current_auth')
class CurrentAuth: """ Holding class for current authenticated objects such as user accounts. This class is constructed by :obj:`current_auth`. Typical uses: Check if you have a valid actor in the current request:: if current_auth: which is equivalent to:: if current_auth.is_authenticated: Reverse check, for anonymous user. Your login manager may or may not treat these as special database objects:: if current_auth.is_anonymous: Access the underlying user object via the :attr:`user` attribute:: if document.user == current_auth.user: other_document.user = current_auth.user If your login manager supports security actors other than users (such as access tokens or client apps), the current actor will be available as the :attr:`actor` attribute. Users are always treated as actors. Additional attributes provided by your login manager are also available as direct attributes of :obj:`current_auth`. """ def __init__(self, user): object.__setattr__(self, 'user', user) object.__setattr__(self, 'actor', user) object.__setattr__(self, 'permissions', InspectableSet()) object.__setattr__( # TODO: Placeholder for anchors self, 'anchors', frozenset() ) def __setattr__(self, attr, value): raise AttributeError('CurrentAuth is read-only') def __repr__(self): # pragma: no cover return 'CurrentAuth(%s)' % repr(self.actor) def __bool__(self): """ Returns ``True`` if user is authenticated, ``False`` if not. """ return self.is_authenticated __nonzero__ = __bool__ # for backward compatibility in Python 2.x @property def is_anonymous(self): """ Property that returns ``True`` if an actor is not present, or if an actor is present but has an ``is_anonymous`` attribute set to ``True``. """ if self.actor is not None: return getattr(self.actor, 'is_anonymous', False) return True @property def not_anonymous(self): """ Shortcut for ```if not current_auth.is_anonymous:```. """ return not self.is_anonymous @property def is_authenticated(self): """ Property that returns ``True`` if an actor is present. """ return self.actor is not None @property def not_authenticated(self): """ Shortcut for ```if not current_auth.is_authenticated:```. """ return not self.is_authenticated def _get_current_auth(): # 1. Do we have a request? if has_request_context(): # 2. Does this request already have current_auth? If so, return it if hasattr(_request_ctx_stack.top, 'current_auth'): return _request_ctx_stack.top.current_auth # 3. If not, does it have a known user (Flask-Login protocol)? If so, construct # current_auth if hasattr(_request_ctx_stack.top, 'user'): _request_ctx_stack.top.current_auth = CurrentAuth( _request_ctx_stack.top.user ) # 4. If none of these, construct a blank one and probe for content else: ca = CurrentAuth(None) # If the login manager below calls :func:`add_auth_attribute`, # we'll have a recursive entry into :func:`_get_current_auth`, so make sure # the stack has an empty :class:`CurrentAuth` on it _request_ctx_stack.top.current_auth = ca # 4.1. Now check for a login manager and call it # Flask-Login, Flask-Lastuser or equivalent must add a login_manager if hasattr(current_app, 'login_manager') and hasattr( current_app.login_manager, '_load_user' ): current_app.login_manager._load_user() # 4.2. In case the login manager did not call :func:`add_auth_attribute`, # we'll need to do it if ca.user is None: add_auth_attribute( 'user', getattr(_request_ctx_stack.top, 'user', None) ) # Return the newly constructed current_auth return _request_ctx_stack.top.current_auth # Fallback if there is no request context. Return a blank current_auth # so that ``current_auth.is_authenticated`` remains valid for checking status return CurrentAuth(None) # Make this work even when there's no request #: A proxy object that hosts state for user authentication, attempting to load #: state from request context if not already loaded. Returns a #: :class:`CurrentAuth`. Typical use:: #: #: from coaster.auth import current_auth #: #: @app.route('/') #: def user_check(): #: if current_auth.is_authenticated: #: return "We have a user" #: else: #: return "User not logged in" current_auth = LocalProxy(_get_current_auth)