From 535542ecfceafcf26a036fd68a7a5923478830b7 Mon Sep 17 00:00:00 2001 From: Mark Stuart Date: Tue, 17 Feb 2026 23:39:07 +1300 Subject: [PATCH 1/3] Update for CKAN 2.11 compatibility - Remove dead CKANLoginThrottle and BeakerRedisAuth classes (repoze.who removed) - Fix SQLAlchemy table.exists() deprecation in model.py (use inspect) - Remove Beaker/repoze.who dependencies from requirements.txt - Loosen redis pin to >=4.1 for compatibility with CKAN core Co-Authored-By: Claude Opus 4.6 --- ckanext/security/authenticator.py | 20 ------------------- ckanext/security/model.py | 15 ++++++++------ .../templates/user/edit_user_form.html | 2 +- requirements.txt | 7 +------ 4 files changed, 11 insertions(+), 33 deletions(-) diff --git a/ckanext/security/authenticator.py b/ckanext/security/authenticator.py index 5a63943..cd7afa0 100644 --- a/ckanext/security/authenticator.py +++ b/ckanext/security/authenticator.py @@ -1,11 +1,9 @@ -from builtins import object import logging from typing import Any, Union from ckan.types import Response from ckan.lib.authenticator import default_authenticate from ckan.model import User -import ckan.plugins as p from ckan.plugins.toolkit import \ request, config, current_user, base, login_user, h, _ from ckan.views.user import next_page_or_default, rotate_token @@ -183,21 +181,3 @@ def login() -> Union[Response, str]: return base.render("user/login.html", extra_vars) return base.render("user/login.html", extra_vars) - - -class CKANLoginThrottle(): - p.implements(p.IAuthenticator) - - def authenticate(self, environ, identity): - return authenticate(identity) - - -class BeakerRedisAuth(object): - p.implements(p.IAuthenticator) - - def authenticate(self, environ, identity): - # At this stage, the identity has already been validated from - # the cookie and redis (use_beaker middleware). We simply return - # the user id from the identity object if it's there, or None if - # the user's identity is not verified. - return identity.get('repoze.who.userid', None) diff --git a/ckanext/security/model.py b/ckanext/security/model.py index 8f5344b..d10211e 100644 --- a/ckanext/security/model.py +++ b/ckanext/security/model.py @@ -10,7 +10,7 @@ from ckan.model import DomainObject, User from ckan.model.meta import metadata, mapper from ckan.plugins import toolkit -from sqlalchemy import Table, Column, types +from sqlalchemy import Table, Column, types, inspect log = logging.getLogger(__name__) user_security_totp = None @@ -20,14 +20,17 @@ def db_setup(): if user_security_totp is None: define_security_tables() - if not model.package_table.exists(): - log.critical("Exiting: can not migrate security model \ -if the database does not exist yet") + db_engine = model.meta.engine + inspector = inspect(db_engine) + + if not inspector.has_table('package'): + log.critical("Exiting: can not migrate security model " + "if the database does not exist yet") sys.exit(1) return - if not user_security_totp.exists(): - user_security_totp.create() + if not inspector.has_table('user_security_totp'): + user_security_totp.create(bind=db_engine) print("Created security TOTP table") else: print("Security TOTP table already exists -- skipping") diff --git a/ckanext/security/templates/user/edit_user_form.html b/ckanext/security/templates/user/edit_user_form.html index 6f2a017..43f2eb4 100644 --- a/ckanext/security/templates/user/edit_user_form.html +++ b/ckanext/security/templates/user/edit_user_form.html @@ -11,7 +11,7 @@ {% if h.security_enable_totp() %}
{{_('Two factor authentication')}} - {% link_for _('Manage two factor authentication'), controller='mfa_user', action='configure_mfa', id=data.id, class_='btn btn-default pull-left', icon='cog' %} + {% link_for _('Manage two factor authentication'), controller='mfa_user', action='configure_mfa', id=data.id, class_='btn btn-secondary float-start', icon='cog' %}
{% endif %} diff --git a/requirements.txt b/requirements.txt index 4a991ac..7539c98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,4 @@ -Beaker~=1.11.0 -beaker-redis~=1.1.0 pyotp~=2.6.0 python-magic~=0.4.24 -redis~=4.1 -repoze.who~=2.4 -git+https://github.com/akissa/repoze.who-use_beaker@780379fd58b10264c0756feb6d3f232f797ba0cb#egg=repoze.who-use_beaker +redis>=4.1 six~=1.16.0 -WebOb~=1.8.7 From ecdbcf9f7aa8db1478b73db58cdafbf3ef0d8ca6 Mon Sep 17 00:00:00 2001 From: Mark Stuart Date: Wed, 18 Feb 2026 16:27:53 +1300 Subject: [PATCH 2/3] Restore 2.9+ compat code, keep only essential 2.11 fixes - Restore CKANLoginThrottle and BeakerRedisAuth classes (needed for CKAN < 2.11 repoze.who-based auth) - Restore Beaker/repoze.who/WebOb requirements (needed for CKAN < 2.11) - Keep SQLAlchemy inspect() fix for 2.11 compat (replaces deprecated table.exists()), using positional arg to create() instead of bind= keyword which was removed in SQLAlchemy 2.0 Co-Authored-By: Claude Sonnet 4.6 --- ckanext/security/authenticator.py | 20 ++++++++++++++++++++ ckanext/security/model.py | 2 +- requirements.txt | 6 ++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/ckanext/security/authenticator.py b/ckanext/security/authenticator.py index cd7afa0..5a63943 100644 --- a/ckanext/security/authenticator.py +++ b/ckanext/security/authenticator.py @@ -1,9 +1,11 @@ +from builtins import object import logging from typing import Any, Union from ckan.types import Response from ckan.lib.authenticator import default_authenticate from ckan.model import User +import ckan.plugins as p from ckan.plugins.toolkit import \ request, config, current_user, base, login_user, h, _ from ckan.views.user import next_page_or_default, rotate_token @@ -181,3 +183,21 @@ def login() -> Union[Response, str]: return base.render("user/login.html", extra_vars) return base.render("user/login.html", extra_vars) + + +class CKANLoginThrottle(): + p.implements(p.IAuthenticator) + + def authenticate(self, environ, identity): + return authenticate(identity) + + +class BeakerRedisAuth(object): + p.implements(p.IAuthenticator) + + def authenticate(self, environ, identity): + # At this stage, the identity has already been validated from + # the cookie and redis (use_beaker middleware). We simply return + # the user id from the identity object if it's there, or None if + # the user's identity is not verified. + return identity.get('repoze.who.userid', None) diff --git a/ckanext/security/model.py b/ckanext/security/model.py index d10211e..cc90baa 100644 --- a/ckanext/security/model.py +++ b/ckanext/security/model.py @@ -30,7 +30,7 @@ def db_setup(): return if not inspector.has_table('user_security_totp'): - user_security_totp.create(bind=db_engine) + user_security_totp.create(db_engine) print("Created security TOTP table") else: print("Security TOTP table already exists -- skipping") diff --git a/requirements.txt b/requirements.txt index 7539c98..25a0967 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ +# Required for CKAN < 2.11 (repoze.who-based auth) +Beaker~=1.11.0 +beaker-redis~=1.1.0 +repoze.who~=2.4 +git+https://github.com/akissa/repoze.who-use_beaker@780379fd58b10264c0756feb6d3f232f797ba0cb#egg=repoze.who-use_beaker +WebOb~=1.8.7 pyotp~=2.6.0 python-magic~=0.4.24 redis>=4.1 From 46c2d31286f388b97784072c776019ffb5968c1b Mon Sep 17 00:00:00 2001 From: Mark Stuart Date: Wed, 18 Feb 2026 16:32:07 +1300 Subject: [PATCH 3/3] Fix logout for CKAN 2.11: use session.clear() if invalidate() unavailable Beaker session (CKAN < 2.11) has invalidate(); Flask-Session (CKAN 2.11+) uses clear(). Duck-type to support both. Co-Authored-By: Claude Sonnet 4.6 --- ckanext/security/plugin/flask_plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ckanext/security/plugin/flask_plugin.py b/ckanext/security/plugin/flask_plugin.py index 8a59a0b..1678624 100644 --- a/ckanext/security/plugin/flask_plugin.py +++ b/ckanext/security/plugin/flask_plugin.py @@ -30,4 +30,8 @@ def authenticate(self, identity): # Delete session cookie information def logout(self): - session.invalidate() + # Beaker session (CKAN < 2.11) uses invalidate(); Flask-Session uses clear() + if hasattr(session, 'invalidate'): + session.invalidate() + else: + session.clear()