''' 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 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): ''' 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)