fix: отозванная cookie не оживает, когда логина нет в users
This commit is contained in:
parent
b4420cea16
commit
a2ce08f46a
2 changed files with 41 additions and 2 deletions
|
|
@ -51,6 +51,8 @@ _INSTANCE = hashlib.sha256(
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
_generations: dict[str, int] = {}
|
_generations: dict[str, int] = {}
|
||||||
_active_sockets: dict[str, weakref.WeakKeyDictionary] = {}
|
_active_sockets: dict[str, weakref.WeakKeyDictionary] = {}
|
||||||
|
# Logins already revoked because their users row disappeared.
|
||||||
|
_vanished: set[str] = set()
|
||||||
AUTH_GENERATION_SYNC_SECONDS = 1.0
|
AUTH_GENERATION_SYNC_SECONDS = 1.0
|
||||||
AUTH_GENERATION_MAX_AGE_SECONDS = 2.0
|
AUTH_GENERATION_MAX_AGE_SECONDS = 2.0
|
||||||
_generations_synced_at: float | None = None
|
_generations_synced_at: float | None = None
|
||||||
|
|
@ -121,10 +123,13 @@ async def sync_generations() -> None:
|
||||||
# The local-only dev-token principal is synthetic, never stored in users;
|
# The local-only dev-token principal is synthetic, never stored in users;
|
||||||
# a DB watcher must not revoke its in-memory generation during test/demo
|
# a DB watcher must not revoke its in-memory generation during test/demo
|
||||||
# flows that deliberately exercise account invalidation.
|
# flows that deliberately exercise account invalidation.
|
||||||
|
# The revoked generation stays cached: dropping it would make the next
|
||||||
|
# lookup fall back to 0 and re-accept cookies issued before the revocation.
|
||||||
synthetic = {"dev"} if get_settings().dev_auth_bypass else set()
|
synthetic = {"dev"} if get_settings().dev_auth_bypass else set()
|
||||||
for login in _generations.keys() - current.keys() - synthetic:
|
_vanished.intersection_update(_generations.keys() - current.keys())
|
||||||
|
for login in _generations.keys() - current.keys() - synthetic - _vanished:
|
||||||
invalidate_login(login)
|
invalidate_login(login)
|
||||||
_generations.pop(login, None)
|
_vanished.add(login)
|
||||||
global _generations_synced_at
|
global _generations_synced_at
|
||||||
_generations_synced_at = time.monotonic()
|
_generations_synced_at = time.monotonic()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,40 @@ def test_peer_node_generation_sync_closes_revoked_websocket(monkeypatch):
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
def test_generation_sync_keeps_revocation_of_login_missing_from_db(monkeypatch):
|
||||||
|
# Демо-логины синтетические, в users их нет. Сброс поколения к 0 после
|
||||||
|
# выхода вернул бы силу cookie, выданной до выхода.
|
||||||
|
login = "demo-instructor"
|
||||||
|
|
||||||
|
class FakeResult:
|
||||||
|
def all(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
class FakeDb:
|
||||||
|
async def execute(self, _query):
|
||||||
|
return FakeResult()
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return FakeDb()
|
||||||
|
|
||||||
|
async def __aexit__(self, *_args):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth, "get_sessionmaker", lambda: lambda: FakeSession())
|
||||||
|
auth.prime_generations({})
|
||||||
|
who = auth._demo_principal(Role.INSTRUCTOR)
|
||||||
|
request = SimpleNamespace(session={})
|
||||||
|
auth._issue_session(request, who)
|
||||||
|
stale = dict(request.session)
|
||||||
|
|
||||||
|
auth.invalidate_login(login)
|
||||||
|
asyncio.run(auth.sync_generations())
|
||||||
|
|
||||||
|
assert auth._session_principal(stale) is None
|
||||||
|
auth._generations.pop(login, None)
|
||||||
|
|
||||||
|
|
||||||
def test_revocation_does_not_log_error_if_socket_already_disconnected():
|
def test_revocation_does_not_log_error_if_socket_already_disconnected():
|
||||||
class DisconnectedSocket:
|
class DisconnectedSocket:
|
||||||
async def close(self, **_kwargs):
|
async def close(self, **_kwargs):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue