# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Authentication & Authorization
"""
from passlib.context import CryptContext
from sqlalchemy.orm.exc import NoResultFound
from rattail.db import model
password_context = CryptContext(schemes=['bcrypt'])
[docs]
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``.
"""
if isinstance(userobj, model.User):
user = userobj
else:
try:
user = session.query(model.User)\
.filter_by(username=userobj)\
.one()
except NoResultFound:
user = None
if user and user.active and user.password is not None:
if password_context.verify(password, user.password):
return user
[docs]
def set_user_password(user, password):
"""
Set a user's password.
"""
user.password = password_context.hash(password)
def special_role(session, uuid, name):
"""
Fetches, or creates, a "special" role.
"""
role = session.get(model.Role, uuid)
if not role:
role = model.Role(uuid=uuid, name=name)
session.add(role)
return role
[docs]
def administrator_role(session):
"""
Returns the "Administrator" role.
"""
return special_role(session, 'd937fa8a965611dfa0dd001143047286', 'Administrator')
[docs]
def guest_role(session):
"""
Returns the "Guest" role.
"""
return special_role(session, 'f8a27c98965a11dfaff7001143047286', 'Guest')
[docs]
def authenticated_role(session):
"""
Returns the "Authenticated" role.
"""
return special_role(session, 'b765a9cc331a11e6ac2a3ca9f40bc550', "Authenticated")
[docs]
def grant_permission(role, permission):
"""
Grant a permission to a role.
"""
# TODO: Make this a `Role` method (or make `Role.permissions` a `set` so we
# can do `role.permissions.add('some.perm')` ?).
if permission not in role.permissions:
role.permissions.append(permission)
[docs]
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.
"""
if permission in role.permissions:
role.permissions.remove(permission)
[docs]
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``.
"""
if hasattr(principal, 'roles'):
roles = list(principal.roles)
if include_authenticated:
roles.append(authenticated_role(session))
elif principal is not None:
roles = [principal]
else:
roles = []
if include_guest:
roles.append(guest_role(session))
for role in roles:
for perm in role.permissions:
if perm == permission:
return True
return False
[docs]
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``.
"""
# 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 = list(principal.roles)
# 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