From 35d68c47eba24aafe74cf9a19b73c509a8685671 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Fri, 14 Aug 2026 11:17:09 +0100 Subject: [PATCH] fix: follow Flask-Security-Too 5.8.2's corrected is_locked() semantics (#10256) Flask-Security-Too 5.8.2, released on 12 August 2026, fixed a long-standing inversion in its login forms: `LoginForm.validate()` previously read a `True` return from `UserMixin.is_locked()` as "not locked, carry on", and the base implementation unconditionally returned `True`. Our `User.is_locked()` was written against that inverted convention, so as soon as CI began resolving 5.8.2 through the loose `Flask-Security-Too==5.8.*` pin, an unlocked user returned `True`, form validation failed, the login POST redirected with a 302 and every subsequent request arrived as `AnonymousUser`. The server-mode data isolation tests caught it, though the breakage is not limited to tests: on 5.8.2 nobody could log in at all. `User.is_locked()` now returns `True` when the account is locked, matching the corrected upstream contract, and the dependency is floored at 5.8.2 so that we cannot silently resolve a release which reads the value backwards. The two conventions are mutually exclusive, hence a floor rather than a version check in the model. The regression tests are updated to assert the fixed contract. --- requirements.txt | 5 ++++- web/pgadmin/model/__init__.py | 15 +++++++++------ .../user_management/tests/test_locked_user.py | 12 +++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7c4ab5b16..98c6c0d60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,10 @@ Flask-Mail==0.* Flask-Migrate==4.* Flask-Paranoid==0.* Flask-Security-Too==5.6.*; python_version <= '3.9' -Flask-Security-Too==5.8.*; python_version > '3.9' +# 5.8.2 corrected the inverted UserMixin.is_locked() test in LoginForm; +# User.is_locked() in web/pgadmin/model/__init__.py follows the fixed +# semantics, so anything earlier in the 5.8 series would refuse all logins. +Flask-Security-Too>=5.8.2,<5.9; python_version > '3.9' Flask-SocketIO==5.6.* Flask-SQLAlchemy==3.1.* Flask-WTF==1.2.*; python_version <= '3.9' diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 49629c3c1..086328829 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -222,17 +222,20 @@ class User(db.Model, UserMixin): def is_locked(self, form_error=None): # Flask-Security's LoginForm.validate() calls this after password - # verification and treats the return value inverted: True means - # "not locked, proceed"; False means "locked, fail validation". - # The default UserMixin.is_locked unconditionally returns True, - # which is what allows the /login bypass on a locked account. + # verification and fails validation when it returns True, so True + # means "locked, refuse the login". Flask-Security-Too up to and + # including 5.8.1 had that test inverted (fixed upstream in 5.8.2 by + # pallets-eco/flask-security#1267), which is why requirements.txt + # floors the dependency at 5.8.2: on an older release the value + # below would be read backwards and every unlocked user would be + # refused a session. if self.locked: if form_error is not None: form_error.append(gettext( 'Your account is locked. Please contact the ' 'Administrator.')) - return False - return True + return True + return False class Setting(db.Model, UserScopedMixin): diff --git a/web/pgadmin/tools/user_management/tests/test_locked_user.py b/web/pgadmin/tools/user_management/tests/test_locked_user.py index b34b22291..0fb51ff62 100644 --- a/web/pgadmin/tools/user_management/tests/test_locked_user.py +++ b/web/pgadmin/tools/user_management/tests/test_locked_user.py @@ -26,8 +26,10 @@ class TestLockedUser(BaseTestGenerator): - `is_active` -> False when the account is deactivated OR locked (Flask-Login's `login_user()` then refuses the session). - - `is_locked(errors)` -> False when locked, True otherwise; populates + - `is_locked(errors)` -> True when locked, False otherwise; populates the supplied error list when locked so the form surfaces a message. + Flask-Security-Too <= 5.8.1 read this the other way round, which is + why requirements.txt floors the dependency at 5.8.2. """ # Pure model-contract test - no Postgres server interaction needed. @@ -39,16 +41,16 @@ class TestLockedUser(BaseTestGenerator): scenarios = [ ('active and not locked: login allowed', dict(active=True, locked=False, - expect_is_active=True, expect_is_locked=True)), + expect_is_active=True, expect_is_locked=False)), ('active and locked: login blocked', dict(active=True, locked=True, - expect_is_active=False, expect_is_locked=False)), + expect_is_active=False, expect_is_locked=True)), ('inactive and not locked: login blocked', dict(active=False, locked=False, - expect_is_active=False, expect_is_locked=True)), + expect_is_active=False, expect_is_locked=False)), ('inactive and locked: login blocked', dict(active=False, locked=True, - expect_is_active=False, expect_is_locked=False)), + expect_is_active=False, expect_is_locked=True)), ] def runTest(self):