Add support for Pyramid 2.x; new security policy

custom apps are still free to use pyramid 1.x

new security policy is only used if config file says so
This commit is contained in:
Lance Edgar 2024-04-16 09:48:29 -05:00
parent 85d62a8e38
commit 8b4b3de336
4 changed files with 91 additions and 14 deletions

View file

@ -49,9 +49,6 @@ install_requires =
# TODO: remove once their bug is fixed? idk what this is about yet... # TODO: remove once their bug is fixed? idk what this is about yet...
deform<2.0.15 deform<2.0.15
# TODO: remove this cap and address warnings that follow
pyramid<2
asgiref asgiref
colander colander
ColanderAlchemy ColanderAlchemy
@ -65,6 +62,7 @@ install_requires =
paginate_sqlalchemy paginate_sqlalchemy
passlib passlib
Pillow Pillow
pyramid
pyramid_beaker>=0.6 pyramid_beaker>=0.6
pyramid_deform pyramid_deform
pyramid_exclog pyramid_exclog

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar # Copyright © 2010-2024 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -133,8 +133,14 @@ def make_pyramid_config(settings, configure_csrf=True):
config.registry['rattail_config'] = rattail_config config.registry['rattail_config'] = rattail_config
# configure user authorization / authentication # configure user authorization / authentication
config.set_authorization_policy(TailboneAuthorizationPolicy()) # TODO: security policy should become the default, for pyramid 2.x
config.set_authentication_policy(SessionAuthenticationPolicy()) 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 # maybe require CSRF token protection
if configure_csrf: if configure_csrf:

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar # Copyright © 2010-2024 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -27,7 +27,6 @@ Authentication & Authorization
import logging import logging
import re import re
from rattail import enum
from rattail.util import prettify, NOTSET from rattail.util import prettify, NOTSET
from zope.interface import implementer 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 Perform the steps necessary to login the given user. Note that this
returns a ``headers`` dict which you should pass to the redirect. 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) headers = remember(request, user.uuid)
if timeout is NOTSET: if timeout is NOTSET:
timeout = session_timeout_for_user(user) 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 Perform the logout action for the given request. Note that this returns a
``headers`` dict which you should pass to the redirect. ``headers`` dict which you should pass to the redirect.
""" """
app = request.rattail_config.get_app()
user = request.user user = request.user
if user: if user:
user.record_event(enum.USER_EVENT_LOGOUT) user.record_event(app.enum.USER_EVENT_LOGOUT)
request.session.delete() request.session.delete()
request.session.invalidate() request.session.invalidate()
headers = forget(request) headers = forget(request)
@ -117,7 +118,7 @@ class TailboneAuthenticationPolicy(SessionAuthenticationPolicy):
return user.uuid return user.uuid
# otherwise do normal session-based logic # otherwise do normal session-based logic
return super(TailboneAuthenticationPolicy, self).unauthenticated_userid(request) return super().unauthenticated_userid(request)
@implementer(IAuthorizationPolicy) @implementer(IAuthorizationPolicy)
@ -150,6 +151,72 @@ class TailboneAuthorizationPolicy(object):
raise NotImplementedError 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): def add_permission_group(config, key, label=None, overwrite=True):
""" """
Add a permission group to the app configuration. Add a permission group to the app configuration.

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar # Copyright © 2010-2024 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -50,8 +50,14 @@ def make_pyramid_config(settings):
pyramid_config = Configurator(settings=settings, root_factory=app.Root) pyramid_config = Configurator(settings=settings, root_factory=app.Root)
# configure user authorization / authentication # configure user authorization / authentication
pyramid_config.set_authentication_policy(TailboneAuthenticationPolicy()) # TODO: security policy should become the default, for pyramid 2.x
pyramid_config.set_authorization_policy(TailboneAuthorizationPolicy()) 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 # always require CSRF token protection
pyramid_config.set_default_csrf_options(require_csrf=True, pyramid_config.set_default_csrf_options(require_csrf=True,