diff --git a/setup.cfg b/setup.cfg index 67541d96..2195aee9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,9 +49,6 @@ install_requires = # TODO: remove once their bug is fixed? idk what this is about yet... deform<2.0.15 - # TODO: remove this cap and address warnings that follow - pyramid<2 - asgiref colander ColanderAlchemy @@ -65,6 +62,7 @@ install_requires = paginate_sqlalchemy passlib Pillow + pyramid pyramid_beaker>=0.6 pyramid_deform pyramid_exclog diff --git a/tailbone/app.py b/tailbone/app.py index ae10c9bc..abf2fa09 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -133,8 +133,14 @@ def make_pyramid_config(settings, configure_csrf=True): config.registry['rattail_config'] = rattail_config # configure user authorization / authentication - config.set_authorization_policy(TailboneAuthorizationPolicy()) - config.set_authentication_policy(SessionAuthenticationPolicy()) + # TODO: security policy should become the default, for pyramid 2.x + if rattail_config.getbool('tailbone', 'pyramid.use_security_policy', + usedb=False, default=False): + from tailbone.auth import TailboneSecurityPolicy + config.set_security_policy(TailboneSecurityPolicy()) + else: + config.set_authorization_policy(TailboneAuthorizationPolicy()) + config.set_authentication_policy(SessionAuthenticationPolicy()) # maybe require CSRF token protection if configure_csrf: diff --git a/tailbone/auth.py b/tailbone/auth.py index 1f057404..66deeff0 100644 --- a/tailbone/auth.py +++ b/tailbone/auth.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -27,7 +27,6 @@ Authentication & Authorization import logging import re -from rattail import enum from rattail.util import prettify, NOTSET from zope.interface import implementer @@ -46,7 +45,8 @@ def login_user(request, user, timeout=NOTSET): Perform the steps necessary to login the given user. Note that this returns a ``headers`` dict which you should pass to the redirect. """ - user.record_event(enum.USER_EVENT_LOGIN) + app = request.rattail_config.get_app() + user.record_event(app.enum.USER_EVENT_LOGIN) headers = remember(request, user.uuid) if timeout is NOTSET: timeout = session_timeout_for_user(user) @@ -60,9 +60,10 @@ def logout_user(request): Perform the logout action for the given request. Note that this returns a ``headers`` dict which you should pass to the redirect. """ + app = request.rattail_config.get_app() user = request.user if user: - user.record_event(enum.USER_EVENT_LOGOUT) + user.record_event(app.enum.USER_EVENT_LOGOUT) request.session.delete() request.session.invalidate() headers = forget(request) @@ -117,7 +118,7 @@ class TailboneAuthenticationPolicy(SessionAuthenticationPolicy): return user.uuid # otherwise do normal session-based logic - return super(TailboneAuthenticationPolicy, self).unauthenticated_userid(request) + return super().unauthenticated_userid(request) @implementer(IAuthorizationPolicy) @@ -150,6 +151,72 @@ class TailboneAuthorizationPolicy(object): raise NotImplementedError +class TailboneSecurityPolicy: + + def __init__(self, api_mode=False): + from pyramid.authentication import SessionAuthenticationHelper + from pyramid.request import RequestLocalCache + + self.api_mode = api_mode + self.session_helper = SessionAuthenticationHelper() + self.identity_cache = RequestLocalCache(self.load_identity) + + def load_identity(self, request): + config = request.registry.settings.get('rattail_config') + app = config.get_app() + user = None + + if self.api_mode: + + # determine/load user from header token if present + credentials = request.headers.get('Authorization') + if credentials: + match = re.match(r'^Bearer (\S+)$', credentials) + if match: + token = match.group(1) + auth = app.get_auth_handler() + user = auth.authenticate_user_token(Session(), token) + + if not user: + + # fetch user uuid from current session + uuid = self.session_helper.authenticated_userid(request) + if not uuid: + return + + # fetch user object from db + model = app.model + user = Session.get(model.User, uuid) + if not user: + return + + # this user is responsible for data changes in current request + Session().set_continuum_user(user) + return user + + def identity(self, request): + return self.identity_cache.get_or_create(request) + + def authenticated_userid(self, request): + user = self.identity(request) + if user is not None: + return user.uuid + + def remember(self, request, userid, **kw): + return self.session_helper.remember(request, userid, **kw) + + def forget(self, request, **kw): + return self.session_helper.forget(request, **kw) + + def permits(self, request, context, permission): + config = request.registry.settings.get('rattail_config') + app = config.get_app() + auth = app.get_auth_handler() + + user = self.identity(request) + return auth.has_permission(Session(), user, permission) + + def add_permission_group(config, key, label=None, overwrite=True): """ Add a permission group to the app configuration. diff --git a/tailbone/webapi.py b/tailbone/webapi.py index 7a2c81b4..70600e79 100644 --- a/tailbone/webapi.py +++ b/tailbone/webapi.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -50,8 +50,14 @@ def make_pyramid_config(settings): pyramid_config = Configurator(settings=settings, root_factory=app.Root) # configure user authorization / authentication - pyramid_config.set_authentication_policy(TailboneAuthenticationPolicy()) - pyramid_config.set_authorization_policy(TailboneAuthorizationPolicy()) + # TODO: security policy should become the default, for pyramid 2.x + if rattail_config.getbool('tailbone', 'pyramid.use_security_policy', + usedb=False, default=False): + from tailbone.auth import TailboneSecurityPolicy + pyramid_config.set_security_policy(TailboneSecurityPolicy(api_mode=True)) + else: + pyramid_config.set_authentication_policy(TailboneAuthenticationPolicy()) + pyramid_config.set_authorization_policy(TailboneAuthorizationPolicy()) # always require CSRF token protection pyramid_config.set_default_csrf_options(require_csrf=True,