|
|
|
''' User Authentication and Authorization '''
|
|
|
|
|
|
|
|
from passlib.context import CryptContext
|
|
|
|
from pyramid.authentication import AuthTktAuthenticationPolicy
|
|
|
|
from pyramid.authorization import ACLAuthorizationPolicy
|
|
|
|
from pyramid.security import Authenticated, Everyone
|
|
|
|
from pyramid.settings import aslist
|
|
|
|
|
|
|
|
from ordr.models.account import User
|
|
|
|
|
|
|
|
#: passlib context for hashing passwords
|
|
|
|
# at least one scheme must be set in advance, will be overridden by the
|
|
|
|
# settings in the .ini file.
|
|
|
|
password_context = CryptContext(schemes=['argon2'])
|
|
|
|
|
|
|
|
|
|
|
|
class AuthenticationPolicy(AuthTktAuthenticationPolicy):
|
|
|
|
''' How to authenticate users '''
|
|
|
|
|
|
|
|
def authenticated_userid(self, request):
|
|
|
|
''' returns the id of an authenticated user
|
|
|
|
|
|
|
|
heavy lifting done in get_user() attached to request
|
|
|
|
'''
|
|
|
|
user = request.user
|
|
|
|
if user is not None:
|
|
|
|
return user.id
|
|
|
|
|
|
|
|
def effective_principals(self, request):
|
|
|
|
''' returns a list of principals for the user '''
|
|
|
|
principals = [Everyone]
|
|
|
|
user = request.user
|
|
|
|
if user is not None:
|
|
|
|
principals.append(Authenticated)
|
|
|
|
principals.extend(user.principals)
|
|
|
|
return principals
|
|
|
|
|
|
|
|
|
|
|
|
def get_user(request):
|
|
|
|
''' retrieves the user object by the unauthenticated user id
|
|
|
|
|
|
|
|
:param pyramid.request.Request request:
|
|
|
|
the current request object
|
|
|
|
:rtype: :class:`ordr.models.account.User` or None
|
|
|
|
'''
|
|
|
|
user_id = request.unauthenticated_userid
|
|
|
|
if user_id is not None:
|
|
|
|
user = request.dbsession.query(User).filter_by(id=user_id).first()
|
|
|
|
if user and user.is_active:
|
|
|
|
return user
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def crypt_context_settings_to_string(settings, prefix='passlib.'):
|
|
|
|
''' returns a passlib context setting as a INI-formatted content
|
|
|
|
|
|
|
|
:param dict settings: settings for the crypt context
|
|
|
|
:param str prefix: prefix of the settings keys
|
|
|
|
:rtype: (str) config string in INI format for CryptContext.load()
|
|
|
|
|
|
|
|
This looks at first like a dump hack, but the parsing of all possible
|
|
|
|
context settings is quite a task. Since passlib has a context parser
|
|
|
|
included, this seems the most reliable way to do it.
|
|
|
|
'''
|
|
|
|
config_lines = ['[passlib]']
|
|
|
|
for ini_key, value in settings.items():
|
|
|
|
if ini_key.startswith(prefix):
|
|
|
|
context_key = ini_key.replace(prefix, '')
|
|
|
|
# the pyramid .ini format is different on lists
|
|
|
|
# than the .ini format used by passlib.
|
|
|
|
if context_key in {'schemes', 'deprecated'} and ',' not in value:
|
|
|
|
value = ','.join(aslist(value))
|
|
|
|
config_lines.append(f'{context_key} = {value}')
|
|
|
|
return '\n'.join(config_lines)
|
|
|
|
|
|
|
|
|
|
|
|
def includeme(config): # pragma: no cover
|
|
|
|
''' initializing authentication, authorization and password hash settings
|
|
|
|
|
|
|
|
Activate this setup using ``config.include('ordr.security')``.
|
|
|
|
'''
|
|
|
|
settings = config.get_settings()
|
|
|
|
|
|
|
|
# configure the passlib context manager for hashing user passwords
|
|
|
|
config_str = crypt_context_settings_to_string(settings, prefix='passlib.')
|
|
|
|
password_context.load(config_str)
|
|
|
|
|
|
|
|
# config for authentication and authorization
|
|
|
|
authn_policy = AuthenticationPolicy(
|
|
|
|
settings.get('auth.secret', ''),
|
|
|
|
hashalg='sha512',
|
|
|
|
)
|
|
|
|
config.set_authentication_policy(authn_policy)
|
|
|
|
config.set_authorization_policy(ACLAuthorizationPolicy())
|
|
|
|
|
|
|
|
# attach the get_user function returned by get_user_closure()
|
|
|
|
config.add_request_method(get_user, 'user', reify=True)
|