Changeset - 1a3693fd10cf
[Not reviewed]
1 10 0
Lance Edgar (lance) - 3 months ago 2024-07-14 23:23:25
lance@edbob.org
feat: move most of auth handler logic to wuttjamaican

also deprecate the `rattail.db.auth` module
11 files changed with 238 insertions and 723 deletions:
0 comments (0 inline, 0 general)
docs/api/rattail/db/auth.rst
Show inline comments
 
deleted file
docs/api/rattail/db/index.rst
Show inline comments
 
@@ -14,7 +14,6 @@
 
   :maxdepth: 1
 
   :caption: Contents:
 

	
 
   auth
 
   cache
 
   changes
 
   config
docs/glossary.rst
Show inline comments
 
@@ -70,14 +70,6 @@ Glossary
 
     least one "default" handler available, though more are possible.  The
 
     batch handler knows how to populate, refresh and execute batches.
 

	
 
   handler
 
     Broadly defined, this refers to an abstraction layer which is meant to
 
     allow for easier customization etc. within the code.  The "handler"
 
     concept allows business rules and/or other logic to be defined in a way
 
     which transcends the app somewhat, but may still be driven by the app.
 
     There are a number of "types" of handlers, e.g. :term:`batch handler` and
 
     :term:`import handler` to name just a couple.
 

	
 
   importer
 
     Class or instance thereof, which contains logic for the import/export of
 
     data for *one specific model*, from one system to another.  Note the
pyproject.toml
Show inline comments
 
@@ -49,7 +49,7 @@ dependencies = [
 
        "texttable",
 
        "typer",
 
        "typing-extensions",
 
        "WuttJamaican>=0.6.1",
 
        "WuttJamaican>=0.8.0",
 
        "xlrd",
 
]
 

	
rattail/app.py
Show inline comments
 
@@ -41,7 +41,9 @@ from functools import partial
 
import humanize
 
from mako.template import Template
 

	
 
from wuttjamaican.app import AppHandler as WuttaAppHandler, AppProvider as WuttaAppProvider
 
from wuttjamaican.app import (AppHandler as WuttaAppHandler,
 
                              AppProvider as WuttaAppProvider,
 
                              GenericHandler as WuttaGenericHandler)
 

	
 
from rattail.util import (load_entry_points,
 
                          progress_loop, prettify,
 
@@ -75,6 +77,8 @@ class AppHandler(WuttaAppHandler):
 
    """
 
    default_app_title = "Rattail"
 
    default_model_spec = 'rattail.db.model'
 
    default_auth_handler_spec = 'rattail.auth:AuthHandler'
 
    default_people_handler_spec = 'rattail.people:PeopleHandler'
 
    default_autocompleters = {
 
        'brands': 'rattail.autocomplete.brands:BrandAutocompleter',
 
        'customers': 'rattail.autocomplete.customers:CustomerAutocompleter',
 
@@ -538,20 +542,6 @@ class AppHandler(WuttaAppHandler):
 
        if interval:
 
            return int(float(interval))
 

	
 
    def get_auth_handler(self, **kwargs):
 
        """
 
        Get the configured "auth" handler.
 

	
 
        :returns: The :class:`~rattail.auth.AuthHandler` instance for
 
           the app.
 
        """
 
        if 'auth' not in self.handlers:
 
            spec = self.config.get('rattail', 'auth.handler',
 
                                   default='rattail.auth:AuthHandler')
 
            factory = self.load_object(spec)
 
            self.handlers['auth'] = factory(self.config, **kwargs)
 
        return self.handlers['auth']
 

	
 
    def get_batch_handler(self, key, default=None, error=True, **kwargs):
 
        """
 
        Get the configured batch handler of the given type.
 
@@ -1011,22 +1001,6 @@ class AppHandler(WuttaAppHandler):
 
            self.handlers['org'] = factory(self.config, **kwargs)
 
        return self.handlers['org']
 

	
 
    def get_people_handler(self, **kwargs):
 
        """
 
        Get the configured "people" handler.
 

	
 
        See also :doc:`rattail-manual:base/handlers/other/people`.
 

	
 
        :returns: The :class:`~rattail.people.PeopleHandler` instance
 
           for the app.
 
        """
 
        if 'people' not in self.handlers:
 
            spec = self.config.get('rattail', 'people.handler',
 
                                   default='rattail.people:PeopleHandler')
 
            factory = self.load_object(spec)
 
            self.handlers['people'] = factory(self.config, **kwargs)
 
        return self.handlers['people']
 

	
 
    def get_poser_handler(self, **kwargs):
 
        """
 
        Get the configured "poser" handler.
 
@@ -1292,15 +1266,6 @@ class AppHandler(WuttaAppHandler):
 
        if phone:
 
            return phone.number
 

	
 
    def get_person(self, obj, **kwargs):
 
        """
 
        Convenience method to locate a Person record for the given
 
        object.  This delegates to the
 
        :class:`~rattail.people.PeopleHandler` for actual lookup
 
        logic.
 
        """
 
        return self.get_people_handler().get_person(obj, **kwargs)
 

	
 
    def get_customer(self, obj, **kwargs):
 
        """
 
        Convenience method to locate a Customer record for the given
 
@@ -2113,18 +2078,23 @@ class AppHandler(WuttaAppHandler):
 
        send_email(self.config, key, data, **kwargs)
 

	
 

	
 
class GenericHandler(object):
 
class GenericHandler(WuttaGenericHandler):
 
    """
 
    Base class for misc. "generic" feature handlers.
 
    Base class for misc. "generic" feature :term:`handlers<handler>`.
 

	
 
    .. warning::
 

	
 
    Most handlers which exist for sake of business logic, should inherit from
 
    this.
 
       This class is slated for deprecation.  New handlers should
 
       subclass :class:`wuttjamaican.app.GenericHandler` instead.
 
    """
 

	
 
    def __init__(self, config, **kwargs):
 
        self.config = config
 
        self.enum = self.config.get_enum()
 
        self.app = self.config.get_app()
 
    # TODO: this is not doing anything that useful, but it must remain
 
    # until all subclasses are updated to avoid self.model etc.
 
    def __init__(self, *args, **kwargs):
 
        super().__init__(*args, **kwargs)
 

	
 
        self.enum = self.app.enum
 

	
 
        try:
 
            import sqlalchemy
 
        except ImportError:
rattail/auth.py
Show inline comments
 
@@ -32,50 +32,75 @@ import warnings
 
from sqlalchemy import orm
 
import sqlalchemy_continuum as continuum
 

	
 
from rattail.app import GenericHandler, MergeMixin
 
from wuttjamaican import auth as base
 

	
 
from rattail.app import MergeMixin
 

	
 
class AuthHandler(GenericHandler, MergeMixin):
 

	
 
class AuthHandler(base.AuthHandler, MergeMixin):
 
    """
 
    Base class and default implementation for the so-called "auth"
 
    handler, by which we mean "authentication and authorization".
 
    Default :term:`auth handler` for Rattail.
 

	
 
    In practice this is also responsible for creating new users, and
 
    various things pertaining to roles, etc.
 
    This is a subclass of :class:`wuttjamaican.auth.AuthHandler` but
 
    adds various methods and logic for Rattail.
 
    """
 

	
 
    def authenticate_user(self, session, username, password):
 
    ##############################
 
    # override methods
 
    ##############################
 

	
 
    def delete_user(self, user, **kwargs):
 
        """
 
        Delete the given user account.  Use with caution!  As this
 
        generally cannot be undone.
 

	
 
        Default behavior here is of course to delete the account, but
 
        it also tries to remove the user association from various
 
        places, in particular the continuum transactions table.
 

	
 
        .. warning::
 

	
 
           Please note that if the user was associated with any
 
           continuum transactions, the "author" for those transactions
 
           will be set to null.
 

	
 
        Depending on the DB schema and data present, deleting the user
 
        may still fail with an error (i.e. if the user is still
 
        referenced by other tables).
 
        """
 
        Authenticate the given user credentials, and if successful,
 
        return the user object.
 
        session = self.app.get_session(user)
 

	
 
        Default logic will (try to) locate a user record with matching
 
        username, then confirm the supplied password is also a match.
 
        # TODO: once we can move the versioning to wuttjamaican, we
 
        # can stop overriding this method altogether
 

	
 
        You may of course define a custom handler and then could
 
        authenticate against anything you like, e.g. the POS system or
 
        LDAP etc.  The only trick is that this must return a Rattail
 
        user, not some other kind.  So you may have to devisde a way
 
        to auto-create the Rattail user as needed, when authentication
 
        for the external system succeeds.
 
        # disassociate user from transactions
 
        if self.config.versioning_has_been_enabled:
 
            self.remove_user_from_continuum_transactions(user)
 

	
 
        Generally speaking the credentials passed in will have come
 
        directly from a user login attempt in the web app etc.  Again
 
        the default logic assumes a "username" but in practice it may
 
        be an email address etc. - whatever the user types.
 
        session.delete(user)
 

	
 
        :param session: Current session for Rattail DB.
 
        # TODO: should make sure no callers are expecting this!
 
        return True
 

	
 
        :param username: Username as string.
 
    ##############################
 
    # extra methods
 
    ##############################
 

	
 
        :param password: Password as string.
 
    def user_is_admin(self, user, **kwargs):
 
        """
 
        Check if given user is a member of the "Administrator" role.
 

	
 
        :returns: On success, a :class:`~rattail.db.model.users.User`
 
           instance; else ``None``.
 
        :rtype: bool
 
        """
 
        from rattail.db.auth import authenticate_user
 
        if not user:
 
            return False
 

	
 
        return authenticate_user(session, username, password)
 
        session = self.app.get_session(user)
 
        admin = self.get_role_administrator(session)
 
        if admin in user.roles:
 
            return True
 

	
 
        return False
 

	
 
    def authenticate_user_token(self, session, token):
 
        """
 
@@ -95,310 +120,6 @@ class AuthHandler(GenericHandler, MergeMixin):
 
            if user.active:
 
                return user
 

	
 
    def get_user(self, obj, **kwargs):
 
        """
 
        Return the User associated with the given object, if any.
 
        """
 
        model = self.model
 

	
 
        if isinstance(obj, model.User):
 
            return obj
 

	
 
        else:
 
            person = self.app.get_person(obj)
 
            if person and person.users:
 
                # TODO: what if multiple users / ambiguous?
 
                return person.users[0]
 

	
 
    def has_permission(self, session, principal, permission,
 
                       include_guest=True,
 
                       include_authenticated=True):
 
        """
 
        Check if the given user or role has been granted the given
 
        permission.
 

	
 
        :param session: Current session for Rattail DB.
 

	
 
        :param principal: Either a
 
           :class:`~rattail.db.model.users.User` or
 
           :class:`~rattail.db.model.users.Role` instance.  It is also
 
           expected that this may sometimes be ``None``, in which case
 
           the "Guest" role will typically be assumed.
 

	
 
        :param permission: Name of the permission for which to check.
 

	
 
        :param include_guest: Whether or not the "Guest" role should
 
           be included when checking permissions.  If ``False``, then
 
           Guest's permissions will *not* be consulted.
 

	
 
        :param include_authenticated: Whether or not the
 
           "Authenticated" role should be included when checking
 
           permissions.
 

	
 
        :returns: Boolean indicating if the permission has been
 
           granted.
 
        """
 
        perms = self.get_permissions(session, principal,
 
                                     include_guest=include_guest,
 
                                     include_authenticated=include_authenticated)
 
        return permission in perms
 

	
 
    def get_permissions(self, session, principal,
 
                          include_guest=True,
 
                          include_authenticated=True):
 
        """
 
        Return a set of permission names, which represents all
 
        permissions effectively granted to the given user or role.
 

	
 
        :param session: Current session for Rattail DB.
 

	
 
        :param principal: Either a
 
           :class:`~rattail.db.model.users.User` or
 
           :class:`~rattail.db.model.users.Role` instance.  It is also
 
           expected that this may sometimes be ``None``, in which case
 
           the "Guest" role will typically be assumed.
 

	
 
        :param include_guest: Whether or not the "Guest" role should
 
           be included when checking permissions.  If ``False``, then
 
           Guest's permissions will *not* be consulted.
 

	
 
        :param include_authenticated: Whether or not the
 
           "Authenticated" role should be included when checking
 
           permissions.
 

	
 
        :returns: Set of permission names.
 
        """
 
        from rattail.db.auth import guest_role, authenticated_role
 

	
 
        # we will use any `roles` attribute which may be present.  in practice we
 
        # would be assuming a User in this case
 
        if hasattr(principal, 'roles'):
 

	
 
            roles = []
 
            for role in principal.roles:
 
                include = False
 
                if role.node_type:
 
                    if role.node_type == self.config.node_type():
 
                        include = True
 
                else:
 
                    include = True
 
                if include:
 
                    roles.append(role)
 

	
 
            # here our User assumption gets a little more explicit
 
            if include_authenticated:
 
                roles.append(authenticated_role(session))
 

	
 
        # otherwise a non-null principal is assumed to be a Role
 
        elif principal is not None:
 
            roles = [principal]
 

	
 
        # fallback assumption is "no roles"
 
        else:
 
            roles = []
 

	
 
        # maybe include guest roles
 
        if include_guest:
 
            roles.append(guest_role(session))
 

	
 
        # build the permissions cache
 
        cache = set()
 
        for role in roles:
 
            cache.update(role.permissions)
 

	
 
        return cache
 

	
 
    def cache_permissions(self, *args, **kwargs): # pragma: no cover
 
        warnings.warn("method is deprecated, please use "
 
                      "get_permissions() method instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.get_permissions(*args, **kwargs)
 

	
 
    def grant_permission(self, role, permission):
 
        """
 
        Grant a permission to the role.  If the role already has the
 
        permission, nothing is done.
 

	
 
        :param role: A :class:`~rattail.db.model.users.Role` instance.
 

	
 
        :param permission: Name of the permission as string.
 
        """
 
        if permission not in role.permissions:
 
            role.permissions.append(permission)
 

	
 
    def revoke_permission(self, role, permission):
 
        """
 
        Revoke a permission from the role.  If the role does not have
 
        the permission, nothing is done.
 

	
 
        :param role: A :class:`~rattail.db.model.users.Role` instance.
 

	
 
        :param permission: Name of the permission as string.
 
        """
 
        if permission in role.permissions:
 
            role.permissions.remove(permission)
 

	
 
    def generate_preferred_username(self, session, **kwargs):
 
        """
 
        Generate a "preferred" username using data from ``kwargs`` as
 
        hints.
 

	
 
        Note that ``kwargs`` should be of the same sort that might be
 
        passed to the constructor for a new
 
        :class:`~rattail.db.model.users.User` instance.
 

	
 
        So far there is only one "hint" which is honored by the
 
        default logic; however the intention is to leave this flexible
 
        as other kinds of hints may be useful in the future.
 

	
 
        This method does not confirm if the username it generates is
 
        actually "available" for a new user.  If you need confirmation
 
        then use :meth:`generate_unique_username()` instead.
 

	
 
        :param session: Current session for Rattail DB.
 

	
 
        :param person: Reference to a
 
           :class:`~rattail.db.model.people.Person` instance.  If you
 
           specify this hint, then default logic will generate a
 
           username using first and last names, like ``'first.last'``.
 
           (You can override with a custom handler if needed.)
 

	
 
        :returns: Generated username as string.
 
        """
 
        person = kwargs.get('person')
 
        if person:
 
            first = (person.first_name or '').strip().lower()
 
            last = (person.last_name or '').strip().lower()
 
            return '{}.{}'.format(first, last)
 

	
 
        return 'newuser'
 

	
 
    def generate_username(self, *args, **kwargs): # pragma: no cover
 
        warnings.warn("method is deprecated, please use "
 
                      "generate_preferred_username() method instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.generate_preferred_username(*args, **kwargs)
 

	
 
    def generate_unique_username(self, session, **kwargs):
 
        """
 
        Generate a *unique* username using data from ``kwargs`` as
 
        hints.
 

	
 
        Note that ``kwargs`` should be of the same sort that might be
 
        passed to the constructor for a new
 
        :class:`~rattail.db.model.users.User` instance.
 

	
 
        This method is a convenience which does two things:
 

	
 
        First it calls :meth:`generate_preferred_username()` to obtain
 
        the "preferred" username.  (It passes ``kwargs`` along when it
 
        makes the call.  See :meth:`generate_preferred_username()` for
 
        more info.)
 

	
 
        Then it checks to see if the resulting username is already
 
        taken.  If it is, then a "counter" is appended to the
 
        username, and incremented until a username can be found which
 
        is *not* yet taken.
 

	
 
        It returns the first "available" (hence unique) username which
 
        is found.  Note that it is considered unique and therefore
 
        available *at the time*; however this method does not
 
        "reserve" the username in any way.  It is assumed that you
 
        would create the user yourself once you have the username.
 

	
 
        :param session: Current session for Rattail DB.
 

	
 
        :returns: Username as string.
 
        """
 
        model = self.model
 

	
 
        original_username = self.generate_preferred_username(session, **kwargs)
 
        username = original_username
 

	
 
        # only if given a session, can we check for unique username
 
        if session:
 
            counter = 1
 
            while True:
 
                users = session.query(model.User)\
 
                               .filter(model.User.username == username)\
 
                               .count()
 
                if not users:
 
                    break
 
                username = "{}{:02d}".format(original_username, counter)
 
                counter += 1
 

	
 
        return username
 

	
 
    def make_user(self, session=None, **kwargs):
 
        """
 
        Make and return a new user.
 

	
 
        This is mostly just a simple wrapper around the normal
 
        :class:`~rattail.db.model.users.User` constructor.  All
 
        ``kwargs`` for instance are passed on to the constructor.
 

	
 
        Default logic here only adds one other convenience:
 

	
 
        If there is no ``username`` specified in the ``kwargs`` then
 
        it will call :meth:`generate_unique_username()` to
 
        automatically provide a username.  Note that all ``kwargs``
 
        are passed along in that call.
 

	
 
        :param session: Current session for the Rattail DB.  This is
 
           "sort of" optional, but please do provide it, as it may
 
           become requied in the future.
 

	
 
        :returns: A new :class:`~rattail.db.model.users.User` instance.
 
        """
 
        model = self.model
 

	
 
        if 'username' not in kwargs:
 
            kwargs['username'] = self.generate_unique_username(session, **kwargs)
 

	
 
        user = model.User(**kwargs)
 
        if session:
 
            session.add(user)
 
        return user
 

	
 
    def get_role(self, session, key, **kwargs):
 
        """
 
        Locate and return a Role for the given key, if possible.
 

	
 
        :param session: App database session.
 

	
 
        :param key: Value to use when searching for the role.  Can
 
           be a UUID or name of a role.
 

	
 
        :returns: The :class:`~rattail.db.model.Role` instance if
 
           found; or ``None``.
 
        """
 
        model = self.model
 

	
 
        # Role.uuid match?
 
        role = session.get(model.Role, key)
 
        if role:
 
            return role
 

	
 
        # Role.name match?
 
        try:
 
            return session.query(model.Role).filter_by(name=key).one()
 
        except orm.exc.NoResultFound:
 
            pass
 

	
 
        # try settings, if value then recurse.
 
        key = self.app.get_setting(session, f'rattail.role.{key}')
 
        if key:
 
            return self.get_role(session, key)
 

	
 
    def get_email_address(self, user, **kwargs):
 
        """
 
        Get the "best" email address we have on file for the given user.
 
        """
 
        warnings.warn("auth.get_email_address(user) is deprecated; please "
 
                      "use app.get_contact_email_address(user) instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.app.get_contact_email_address(user)
 

	
 
    def get_short_display_name(self, user, **kwargs):
 
        """
 
        Returns "short display name" for the user.  This is for
 
@@ -512,37 +233,6 @@ class AuthHandler(GenericHandler, MergeMixin):
 
        for note in notes:
 
            note.created_by = keeping
 

	
 
    def delete_user(self, user, **kwargs):
 
        """
 
        Delete the given user account.  Use with caution!  As this
 
        generally cannot be undone.
 

	
 
        Default behavior here is of course to delete the account, but
 
        it also must try to "remove" the user association from various
 
        places, in particular the continuum transactions table.
 
        Please note that this will leave certain record versions as
 
        appearing to be "without an author".
 

	
 
        :param user: Reference to a
 
           :class:`~rattail.db.model.users.User` to be deleted.
 

	
 
        :returns: Boolean indicating success.
 

	
 
           Note that the utility of this method even having a return
 
           value is deemed questionable, so it's possible in the
 
           future this may just return ``None`` on success, and raise
 
           an error to indicate failure.
 
        """
 
        session = self.app.get_session(user)
 

	
 
        # disassociate user from transactions
 
        if self.config.versioning_has_been_enabled:
 
            self.remove_user_from_continuum_transactions(user)
 

	
 
        # finally, delete the user outright
 
        session.delete(user)
 
        return True
 

	
 
    def remove_user_from_continuum_transactions(self, user):
 
        """
 
        Remove the given user from all Continuum transactions,
 
@@ -565,3 +255,72 @@ class AuthHandler(GenericHandler, MergeMixin):
 
                              .all()
 
        for txn in transactions:
 
            txn.user_id = None
 

	
 
    ##############################
 
    # deprecated methods
 
    ##############################
 

	
 
    def cache_permissions(self, *args, **kwargs): # pragma: no cover
 
        warnings.warn("method is deprecated, please use "
 
                      "get_permissions() method instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.get_permissions(*args, **kwargs)
 

	
 
    def generate_preferred_username(self, *args, **kwargs):
 
        """ """
 
        warnings.warn("generate_preferred_username() is deprecated; "
 
                      "please use make_preferred_username() instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.make_preferred_username(*args, **kwargs)
 

	
 
    def generate_unique_username(self, session, **kwargs):
 
        """ """
 
        warnings.warn("generate_unique_username() is deprecated; "
 
                      "please use make_unique_username() instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.make_unique_username(*args, **kwargs)
 

	
 
    def generate_username(self, *args, **kwargs): # pragma: no cover
 
        """ """
 
        warnings.warn("method is deprecated, please use "
 
                      "generate_preferred_username() method instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.generate_preferred_username(*args, **kwargs)
 

	
 
    def get_email_address(self, user, **kwargs):
 
        """ """
 
        warnings.warn("auth.get_email_address(user) is deprecated; please "
 
                      "use app.get_contact_email_address(user) instead",
 
                      DeprecationWarning, stacklevel=2)
 
        return self.app.get_contact_email_address(user)
 

	
 
    # nb. technically the method is not deprecated, just a kwarg
 
    def get_permissions(self, *args, **kwargs):
 
        """ """
 
        if 'include_guest' in kwargs:
 
            warnings.warn("the include_guest param is deprecated; "
 
                          "please use include_anonymous instead",
 
                          DeprecationWarning, stacklevel=2)
 
            kwargs.setdefault('include_anonymous', kwargs.pop('include_guest'))
 
        return super().get_permissions(*args, **kwargs)
 

	
 
    # nb. technically the method is not deprecated, just a kwarg
 
    def has_permission(self, *args, **kwargs):
 
        """ """
 
        if 'include_guest' in kwargs:
 
            warnings.warn("the include_guest param is deprecated; "
 
                          "please use include_anonymous instead",
 
                          DeprecationWarning, stacklevel=2)
 
            kwargs.setdefault('include_anonymous', kwargs.pop('include_guest'))
 
        return super().has_permission(*args, **kwargs)
 

	
 
    ##############################
 
    # internal methods
 
    ##############################
 

	
 
    def _role_is_pertinent(self, role):
 
        if role.node_type:
 
            if role.node_type == self.config.node_type():
 
                return True
 
            return False
 
        return True
rattail/db/auth.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2023 Lance Edgar
 
#  Copyright © 2010-2024 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -21,11 +21,15 @@
 
#
 
################################################################################
 
"""
 
Authentication & Authorization
 
DEPRECATED: Authentication & Authorization
 

	
 
This entire module has been deprecated; please use the :term:`auth
 
handler` instead.
 
"""
 

	
 
import warnings
 

	
 
from passlib.context import CryptContext
 
from sqlalchemy.orm.exc import NoResultFound
 

	
 
from rattail.db import model
 

	
 
@@ -35,23 +39,21 @@ password_context = CryptContext(schemes=['bcrypt'])
 

	
 
def authenticate_user(session, userobj, password):
 
    """
 
    Attempt to authenticate a user.
 

	
 
    :param userobj: May be a :class:`model.User` instance, or a username as
 
       string.  If the latter, it will be used to look up the User instance.
 

	
 
    :returns: The User instance, if found and the password was correct;
 
       otherwise ``None``.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.authenticate_user()`
 
    instead.
 
    """
 
    warnings.warn("authenticate_user() function is deprecated; "
 
                  "please use AuthHandler.authenticate_user() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    if isinstance(userobj, model.User):
 
        user = userobj
 
    else:
 
        try:
 
            user = session.query(model.User)\
 
                          .filter_by(username=userobj)\
 
                          .one()
 
        except NoResultFound:
 
            user = None
 
        user = session.query(model.User)\
 
                      .filter_by(username=userobj)\
 
                      .first()
 

	
 
    if user and user.active and user.password is not None:
 
        if password_context.verify(password, user.password):
 
            return user
 
@@ -59,15 +61,22 @@ def authenticate_user(session, userobj, password):
 

	
 
def set_user_password(user, password):
 
    """
 
    Set a user's password.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.set_user_password()`
 
    instead.
 
    """
 
    warnings.warn("set_user_password() function is deprecated; "
 
                  "please use AuthHandler.set_user_password() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    user.password = password_context.hash(password)
 

	
 

	
 
def special_role(session, uuid, name):
 
    """
 
    Fetches, or creates, a "special" role.
 
    """
 
    """ """
 
    warnings.warn("special_role() function is deprecated; "
 
                  "please use AuthHandler._special_role() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    role = session.get(model.Role, uuid)
 
    if not role:
 
@@ -78,31 +87,52 @@ def special_role(session, uuid, name):
 

	
 
def administrator_role(session):
 
    """
 
    Returns the "Administrator" role.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.get_role_administrator()`
 
    instead.
 
    """
 
    warnings.warn("administrator_role() function is deprecated; "
 
                  "please use AuthHandler.get_role_administrator() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    return special_role(session, 'd937fa8a965611dfa0dd001143047286', 'Administrator')
 

	
 

	
 
def guest_role(session):
 
    """
 
    Returns the "Guest" role.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.get_role_anonymous()`
 
    instead.
 
    """
 
    warnings.warn("guest_role() function is deprecated; "
 
                  "please use AuthHandler.get_role_anonymous() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    return special_role(session, 'f8a27c98965a11dfaff7001143047286', 'Guest')
 

	
 

	
 
def authenticated_role(session):
 
    """
 
    Returns the "Authenticated" role.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.get_role_authenticated()`
 
    instead.
 
    """
 
    warnings.warn("authenticated_role() function is deprecated; "
 
                  "please use AuthHandler.get_role_authenticated() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    return special_role(session, 'b765a9cc331a11e6ac2a3ca9f40bc550', "Authenticated")
 

	
 

	
 
def grant_permission(role, permission):
 
    """
 
    Grant a permission to a role.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.grant_permission()`
 
    instead.
 
    """
 
    warnings.warn("grant_permission() function is deprecated; "
 
                  "please use AuthHandler.grant_permission() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    # TODO: Make this a `Role` method (or make `Role.permissions` a `set` so we
 
    # can do `role.permissions.add('some.perm')` ?).
 
@@ -112,38 +142,27 @@ def grant_permission(role, permission):
 

	
 
def revoke_permission(role, permission):
 
    """
 
    Revoke the given permission for the given role.  This first checks to see
 
    if the role currently has the permission; if not then no change is made.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.revoke_permission()`
 
    instead.
 
    """
 
    warnings.warn("revoke_permission() function is deprecated; "
 
                  "please use AuthHandler.revoke_permission() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    if permission in role.permissions:
 
        role.permissions.remove(permission)
 

	
 

	
 
def has_permission(session, principal, permission, include_guest=True, include_authenticated=True):
 
    """
 
    Determine if a principal has been granted a permission.
 

	
 
    :param session: A SQLAlchemy session instance.
 

	
 
    :param principal: May be either a :class:`.model.User` or
 
       :class:`.model.Role` instance.  It is also expected that this may
 
       sometimes be ``None``, in which case the "Guest" role will typically be
 
       assumed.
 

	
 
    :param permission: The full internal name of a permission,
 
       e.g. ``'users.create'``.
 

	
 
    :param include_guest: Whether or not the "Guest" role should be included
 
       when checking permissions.  If ``False``, then Guest's permissions will
 
       *not* be consulted.
 

	
 
    :param include_authenticated: Whether or not the "Authenticated" role
 
       should be included when checking permissions.
 

	
 
    Note that if no ``principal`` is provided, and ``include_guest`` is set to
 
    ``False``, then no checks will actually be done, and the return value will
 
    be ``False``.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.has_permission()`
 
    instead.
 
    """
 
    warnings.warn("has_permission() function is deprecated; "
 
                  "please use AuthHandler.has_permission() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    if hasattr(principal, 'roles'):
 
        roles = list(principal.roles)
 
@@ -165,27 +184,14 @@ def has_permission(session, principal, permission, include_guest=True, include_a
 

	
 
def cache_permissions(session, principal, include_guest=True, include_authenticated=True):
 
    """
 
    Return a set of permission names, which represents all permissions
 
    effectively granted to the given principal.
 

	
 
    :param session: A SQLAlchemy session instance.
 

	
 
    :param principal: May be either a :class:`.model.User` or
 
       :class:`.model.Role` instance.  It is also expected that this may
 
       sometimes be ``None``, in which case the "Guest" role will typically be
 
       assumed.
 

	
 
    :param include_guest: Whether or not the "Guest" role should be included
 
       when checking permissions.  If ``False``, then Guest's permissions will
 
       *not* be consulted.
 

	
 
    :param include_authenticated: Whether or not the "Authenticated" role
 
       should be included when checking permissions.
 

	
 
    Note that if no ``principal`` is provided, and ``include_guest`` is set to
 
    ``False``, then no checks will actually be done, and the return value will
 
    be ``False``.
 
    DEPRECATED; use
 
    :meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.get_permissions()`
 
    instead.
 
    """
 
    warnings.warn("cache_permissions() function is deprecated; "
 
                  "please use AuthHandler.get_permissions() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    # we will use any `roles` attribute which may be present.  in practice we
 
    # would be assuming a User in this case
 
    if hasattr(principal, 'roles'):
rattail/db/model/users.py
Show inline comments
 
@@ -300,9 +300,13 @@ class User(Base):
 

	
 
    def is_admin(self):
 
        """
 
        Convenience method to determine if current user is a member of the
 
        Administrator role.
 
        DEPRECATED; use
 
        :meth:`rattail.auth.AuthHandler.user_is_admin()` instead.
 
        """
 
        warnings.warn("user.is_admin() is deprecated; "
 
                      "please use AuthHandler.user_is_admin(user) instead",
 
                      DeprecationWarning, stacklevel=2)
 

	
 
        from rattail.db.auth import administrator_role
 

	
 
        session = orm.object_session(self)
rattail/people.py
Show inline comments
 
@@ -28,10 +28,12 @@ See also :doc:`rattail-manual:base/handlers/other/people`.
 

	
 
import warnings
 

	
 
from rattail.app import GenericHandler, MergeMixin
 
from wuttjamaican import people as base
 

	
 
from rattail.app import MergeMixin
 

	
 
class PeopleHandler(GenericHandler, MergeMixin):
 

	
 
class PeopleHandler(base.PeopleHandler, MergeMixin):
 
    """
 
    Base class and default implementation for people handlers.
 
    """
tests/test_app.py
Show inline comments
 
@@ -427,16 +427,6 @@ class TestAppHandler(TestCase):
 
        membership02 = self.app.get_membership_handler()
 
        self.assertIs(membership02, membership01)
 

	
 
    def test_get_people_handler(self):
 

	
 
        # first call gets the default handler
 
        people01 = self.app.get_people_handler()
 
        self.assertIsNotNone(people01)
 

	
 
        # second call gets the same handler instance
 
        people02 = self.app.get_people_handler()
 
        self.assertIs(people02, people01)
 

	
 
    def test_get_products_handler(self):
 

	
 
        # first call gets the default handler
tests/test_auth.py
Show inline comments
 
@@ -8,8 +8,6 @@ try:
 
    import sqlalchemy as sa
 
    from rattail import auth as mod
 
    from rattail.db import Session
 
    from rattail.db.auth import (set_user_password, administrator_role, 
 
                                 guest_role, authenticated_role)
 
except ImportError:
 
    pass
 
else:
 
@@ -27,88 +25,6 @@ else:
 
        def make_handler(self):
 
            return mod.AuthHandler(self.config)
 

	
 
        def test_authenticate_user(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # should fail if there are no users!
 
            result = self.handler.authenticate_user(session, 'myuser', 'mypass')
 
            self.assertIsNone(result)
 

	
 
            # okay now add a user and make sure it does work
 
            myuser = model.User(username='myuser')
 
            set_user_password(myuser, 'mypass')
 
            session.add(myuser)
 
            result = self.handler.authenticate_user(session, 'myuser', 'mypass')
 
            self.assertIsInstance(result, model.User)
 
            self.assertEqual(result.username, 'myuser')
 

	
 
        def test_generate_preferred_username(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # default is just hard-coded
 
            result = self.handler.generate_preferred_username(session)
 
            self.assertEqual(result, 'newuser')
 

	
 
            # but if we specify a person then will return 'first.last'
 
            person = model.Person(first_name='Fred', last_name='Flintstone')
 
            result = self.handler.generate_preferred_username(session, person=person)
 
            self.assertEqual(result, 'fred.flintstone')
 

	
 
        def test_generate_unique_username(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # default is just hard-coded
 
            result = self.handler.generate_unique_username(session)
 
            self.assertEqual(result, 'newuser')
 

	
 
            # unless we make a user with that name, then it must use a counter
 
            user = model.User(username='newuser')
 
            session.add(user)
 
            result = self.handler.generate_unique_username(session)
 
            self.assertEqual(result, 'newuser01')
 

	
 
            # if we specify a person then will return 'first.last'
 
            person = model.Person(first_name='Fred', last_name='Flintstone')
 
            result = self.handler.generate_unique_username(session, person=person)
 
            self.assertEqual(result, 'fred.flintstone')
 

	
 
            # unless username is taken, in which case it must use a counter
 
            user = model.User(username='fred.flintstone')
 
            session.add(user)
 
            person = model.Person(first_name='Fred', last_name='Flintstone')
 
            result = self.handler.generate_unique_username(session, person=person)
 
            self.assertEqual(result, 'fred.flintstone01')
 

	
 
        def test_make_user(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # making a user with no info at all, gets hard-coded username
 
            user = self.handler.make_user()
 
            self.assertIsInstance(user, model.User)
 
            self.assertEqual(user.username, 'newuser')
 

	
 
            # or we can specify the username directly
 
            user = self.handler.make_user(username='foobar')
 
            self.assertIsInstance(user, model.User)
 
            self.assertEqual(user.username, 'foobar')
 

	
 
            # if we specify a person then username will be like 'first.last'
 
            person = model.Person(first_name='Fred', last_name='Flintstone')
 
            user = self.handler.make_user(session, person=person)
 
            self.assertEqual(user.username, 'fred.flintstone')
 

	
 
        def test_delete_user(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
@@ -124,125 +40,25 @@ else:
 
            session.commit()
 
            self.assertNotIn(user, session)
 

	
 
        def test_has_permission(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # anonymous does not have any permission by default
 
            result = self.handler.has_permission(session, None, 'common.feedback')
 
            self.assertFalse(result)
 

	
 
            # make a role and user, but each should still not have permission
 
            role = model.Role(name='foobar')
 
            user = model.User(username='whatever')
 
            user.roles.append(role)
 
            session.add(user)
 
            result = self.handler.has_permission(session, role, 'common.feedback')
 
            self.assertFalse(result)
 
            result = self.handler.has_permission(session, user, 'common.feedback')
 
            self.assertFalse(result)
 

	
 
            # grant permission, then check again
 
            role.permissions.append('common.feedback')
 
            result = self.handler.has_permission(session, role, 'common.feedback')
 
            self.assertTrue(result)
 
            result = self.handler.has_permission(session, user, 'common.feedback')
 
            self.assertTrue(result)
 

	
 
        def test_get_permissions(self):
 
        def test_user_is_admin(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # admin does not have any permissions by default
 
            admin = administrator_role(session)
 
            result = self.handler.get_permissions(session, admin)
 
            self.assertEqual(result, set())
 
            # no user
 
            self.assertFalse(self.handler.user_is_admin(None))
 

	
 
            # we can grant perm to guest, and then all can inherit
 
            guest = guest_role(session)
 
            guest.permissions.append('common.feedback')
 
            result = self.handler.get_permissions(session, None)
 
            self.assertEqual(result, set(['common.feedback']))
 

	
 
            # make a user, make sure it gets same perms
 
            user = model.User(username='betty')
 
            # real user but not an admin
 
            user = model.User(username='barney')
 
            session.add(user)
 
            result = self.handler.get_permissions(session, user)
 
            self.assertEqual(result, set(['common.feedback']))
 

	
 
            # but it has no perms if we exclude guest when checking
 
            result = self.handler.get_permissions(session, user, include_guest=False)
 
            self.assertEqual(result, set())
 

	
 
            # grant perms to authenticated, make sure that works
 
            authd = authenticated_role(session)
 
            authd.permissions.append('common.consume_batch_id')
 
            result = self.handler.get_permissions(session, user, include_guest=False)
 
            self.assertEqual(result, set(['common.consume_batch_id']))
 

	
 
            # and user still does not have perms if we exclude authenticated
 
            result = self.handler.get_permissions(session, user, include_guest=False,
 
                                                  include_authenticated=False)
 
            self.assertEqual(result, set())
 

	
 
            # finally add user to new role, make sure all works
 
            role = model.Role(name='Site Admin')
 
            user.roles.append(role)
 
            role.permissions.append('common.change_app_theme')
 
            result = self.handler.get_permissions(session, user)
 
            self.assertEqual(result, set(['common.change_app_theme',
 
                                          'common.consume_batch_id', 
 
                                          'common.feedback']))
 

	
 
            # now let's set a node type and corresponding roles, grant some 
 
            # selective perms, then make sure all comes back okay
 
            self.config.setdefault('rattail', 'node_type', 'type1')
 
            type1_users = model.Role(name='Node Type 1 Users', node_type='type1')
 
            type2_users = model.Role(name='Node Type 2 Users', node_type='type2')
 
            user.roles.extend([type1_users, type2_users])
 
            type1_users.permissions.append('products.list')
 
            type2_users.permissions.append('customers.list')
 

	
 
            # our node is configured as type 1, so even though user belongs to
 
            # type2 role it should not inherit that permission on *this* node
 
            result = self.handler.get_permissions(session, user)
 
            self.assertEqual(result, set(['common.change_app_theme', 
 
                                          'common.consume_batch_id',
 
                                          'common.feedback',
 
                                          'products.list']))
 

	
 
        def test_grant_permission(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 

	
 
            # make a role, grant a perm, confirm
 
            role = model.Role(name='FooBar')
 
            self.assertEqual(role.permissions, [])
 
            self.handler.grant_permission(role, 'perm01')
 
            self.assertEqual(role.permissions, ['perm01'])
 

	
 
            # make sure it doesn't double-add
 
            self.handler.grant_permission(role, 'perm01')
 
            self.assertEqual(role.permissions, ['perm01'])
 

	
 
        def test_revoke_permission(self):
 
            engine = sa.create_engine('sqlite://')
 
            model = self.app.model
 
            model.Base.metadata.create_all(bind=engine)
 
            session = Session(bind=engine)
 
            session.commit()
 
            self.assertFalse(self.handler.user_is_admin(user))
 

	
 
            # make a role, grant perms, then revoke one and check
 
            role = model.Role(name='FooBar')
 
            role.permissions.extend(['perm01', 'perm02'])
 
            self.handler.revoke_permission(role, 'perm01')
 
            self.assertEqual(role.permissions, ['perm02'])
 
            # but if they are admin, it shows
 
            admin = self.handler.get_role_administrator(session)
 
            user.roles.append(admin)
 
            session.commit()
 
            self.assertTrue(self.handler.user_is_admin(user))
 

	
 
            # make sure it doesn't try to somehow double-revoke
 
            self.handler.revoke_permission(role, 'perm01')
 
            self.assertEqual(role.permissions, ['perm02'])
 
            session.close()
0 comments (0 inline, 0 general)