mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-17 16:34:44 -05:00
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.
This commit is contained in:
+4
-1
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user