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...
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

View file

@ -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:

View file

@ -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.

View file

@ -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,