"""Session timeouts and the roles refresh, enforced by the server.""" from __future__ import annotations from pathlib import Path from fakedir import FakeDirectory from fastapi.testclient import TestClient from pshelpers import ORIGIN_HEADERS, app_conn, make_settings, needs_pg, sign_in from packetstation.auth import sessions from packetstation.settings import Settings pytestmark = needs_pg def _age(settings: Settings, sql: str) -> None: with app_conn(settings) as conn: conn.execute(sql) def test_token_is_stored_only_as_hmac(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "clerk.clara") token = pg_client.cookies.get(sessions.SESSION_COOKIE) assert token key = pg_settings.session_key() with app_conn(pg_settings) as conn: stored = bytes(conn.execute("SELECT token_hash FROM sessions").fetchone()[0]) assert stored == sessions.token_hash(key, token) and token.encode() not in stored def test_idle_lock_after_lock_minutes(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "wh.wade") _age(pg_settings, "UPDATE sessions SET last_seen_at = now() - interval '4 minutes'") r = pg_client.get("/api/account") assert r.status_code == 423 and r.json()["code"] == "locked" assert pg_client.get("/api/auth/me").json()["session"]["locked"] is True with app_conn(pg_settings) as conn: assert conn.execute("SELECT locked_since IS NOT NULL FROM sessions").fetchone()[0] lock = conn.execute("SELECT detail FROM audit_events WHERE action = 'auth.lock'").fetchone()[0] assert lock == {"source": "idle"} def test_hidden_tab_locks_only_after_a_minute(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "clerk.clara") # A reload fires a hidden report and the next request follows at once: no lock assert pg_client.post("/api/auth/visibility", json={"hidden": True}, headers=ORIGIN_HEADERS).status_code == 204 assert pg_client.get("/api/account").status_code == 200 with app_conn(pg_settings) as conn: assert conn.execute("SELECT hidden_since IS NULL AND locked_since IS NULL FROM sessions").fetchone()[0] # Hidden for over a minute: the visible report (or any request) locks assert pg_client.post("/api/auth/visibility", json={"hidden": True}, headers=ORIGIN_HEADERS).status_code == 204 _age(pg_settings, "UPDATE sessions SET hidden_since = now() - interval '61 seconds'") assert pg_client.post("/api/auth/visibility", json={"hidden": False}, headers=ORIGIN_HEADERS).status_code == 204 r = pg_client.get("/api/account") assert r.status_code == 423 and r.json()["code"] == "locked" with app_conn(pg_settings) as conn: lock = conn.execute("SELECT detail FROM audit_events WHERE action = 'auth.lock'").fetchone()[0] assert lock == {"source": "hidden"} def test_idle_signout_after_idle_minutes(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "wh.wade") _age(pg_settings, "UPDATE sessions SET last_seen_at = now() - interval '16 minutes'") r = pg_client.get("/api/auth/me") assert r.status_code == 401 and r.json()["code"] == "session_expired" assert pg_client.get("/api/auth/me").json()["code"] == "no_session" with app_conn(pg_settings) as conn: assert conn.execute("SELECT revoked_reason FROM sessions").fetchone()[0] == "idle" ended = conn.execute("SELECT detail FROM audit_events WHERE action = 'auth.session_ended'").fetchone()[0] assert ended == {"reason": "idle"} def test_admin_and_clerical_get_desk_minutes(pg_client: TestClient, pg_settings: Settings) -> None: # Issue #45: Admin and Clerical lock and sign out after 60 idle minutes, not 3 and 15 for login in ("clerk.clara", "clerk.leo", "admin.ann", "multi.mia"): pg_client.cookies.clear() sign_in(pg_client, login) assert pg_client.get("/api/auth/me").json()["session"]["lock_minutes"] == 60 _age(pg_settings, "UPDATE sessions SET last_seen_at = now() - interval '59 minutes' WHERE revoked_at IS NULL") assert pg_client.get("/api/account").status_code == 200, login _age(pg_settings, "UPDATE sessions SET last_seen_at = now() - interval '61 minutes' WHERE revoked_at IS NULL") r = pg_client.get("/api/auth/me") assert r.status_code == 401 and r.json()["code"] == "session_expired", login def test_shop_screens_keep_short_minutes(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "weld.andy") assert pg_client.get("/api/auth/me").json()["session"]["lock_minutes"] == 3 def test_locked_session_does_not_stay_alive_by_polling(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "clerk.clara") pg_client.post("/api/auth/lock") _age(pg_settings, "UPDATE sessions SET last_seen_at = now() - interval '14 minutes'") assert pg_client.get("/api/auth/me").status_code == 200 with app_conn(pg_settings) as conn: age = conn.execute("SELECT now() - last_seen_at > interval '13 minutes' FROM sessions").fetchone()[0] assert age # /me while locked does not count as activity def test_last_seen_updates_after_30_seconds(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "clerk.clara") _age(pg_settings, "UPDATE sessions SET last_seen_at = now() - interval '2 minutes'") pg_client.get("/api/account") with app_conn(pg_settings) as conn: assert conn.execute("SELECT now() - last_seen_at < interval '10 seconds' FROM sessions").fetchone()[0] def test_absolute_expiry(pg_client: TestClient, pg_settings: Settings) -> None: sign_in(pg_client, "clerk.clara") _age(pg_settings, "UPDATE sessions SET expires_at = now() - interval '1 second'") r = pg_client.get("/api/auth/me") assert r.status_code == 401 and r.json()["code"] == "no_session" def test_session_hours_are_settings( pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None, tmp_path: Path ) -> None: from pshelpers import ORIGIN_HEADERS from packetstation.main import create_app s = make_settings( tmp_path / "s2", **{k: getattr(pg_settings, k) for k in ("pg_host", "pg_port", "pg_database")}, pg_app_password_file=pg_settings.pg_app_password_file, pg_migrate_password_file=pg_settings.pg_migrate_password_file, session_password_hours=3, ) with TestClient(create_app(s, directory=fake_dir), headers=ORIGIN_HEADERS) as c: me = sign_in(c, "clerk.clara").json() with app_conn(pg_settings) as conn: hours = conn.execute( "SELECT extract(epoch FROM expires_at - created_at) / 3600 FROM sessions WHERE id = %s", (me["session"]["id"],), ).fetchone()[0] assert float(hours) == 3 def test_roles_refresh_rewrites_snapshot(pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory) -> None: assert sign_in(pg_client, "weld.andy").json()["roles"] == ["shop:03:worker"] fake_dir.user("weld.andy").groups = ["PS-03-WeldParts-Lead", "PS-QC"] assert pg_client.get("/api/auth/me").json()["roles"] == ["shop:03:worker"] # not due yet _age(pg_settings, "UPDATE sessions SET roles_checked_at = now() - interval '16 minutes'") me = pg_client.get("/api/auth/me").json() assert me["roles"] == ["qc", "shop:03:lead"] and me["screens"] == ["supervisor", "stall", "qc"] with app_conn(pg_settings) as conn: rows = conn.execute("SELECT role::text, dept_code FROM user_roles ORDER BY role").fetchall() assert rows == [("qc", "QC"), ("shop_lead", "03")] snap = conn.execute("SELECT roles_snapshot FROM sessions").fetchone()[0] assert snap["groups"] == ["PS-03-WeldParts-Lead", "PS-QC"] def test_roles_refresh_ends_session_when_roles_gone( pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory ) -> None: sign_in(pg_client, "weld.andy") fake_dir.user("weld.andy").groups = [] _age(pg_settings, "UPDATE sessions SET roles_checked_at = now() - interval '16 minutes'") r = pg_client.get("/api/auth/me") assert r.status_code == 401 and r.json()["code"] == "session_expired" with app_conn(pg_settings) as conn: assert conn.execute("SELECT revoked_reason FROM sessions").fetchone()[0] == "roles_lost" assert conn.execute("SELECT count(*) FROM user_roles").fetchone()[0] == 0 assert conn.execute("SELECT active FROM users").fetchone()[0] is True def test_roles_refresh_disabled_account(pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory) -> None: sign_in(pg_client, "weld.andy") fake_dir.user("weld.andy").disabled = True _age(pg_settings, "UPDATE sessions SET roles_checked_at = now() - interval '16 minutes'") assert pg_client.get("/api/auth/me").json()["code"] == "session_expired" with app_conn(pg_settings) as conn: assert conn.execute("SELECT active FROM users").fetchone()[0] is False def test_roles_refresh_keeps_roles_when_ad_down( pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory ) -> None: sign_in(pg_client, "weld.andy") fake_dir.down = True _age(pg_settings, "UPDATE sessions SET roles_checked_at = now() - interval '16 minutes'") assert pg_client.get("/api/auth/me").json()["roles"] == ["shop:03:worker"] with app_conn(pg_settings) as conn: assert conn.execute("SELECT now() - roles_checked_at < interval '1 minute' FROM sessions").fetchone()[0] def test_signout_others(pg_client: TestClient, pg_settings: Settings, fake_dir: FakeDirectory) -> None: from pshelpers import ORIGIN_HEADERS from packetstation.main import create_app with TestClient(create_app(pg_settings, directory=fake_dir), headers=ORIGIN_HEADERS) as other: sign_in(other, "clerk.clara") sign_in(pg_client, "clerk.clara") listed = pg_client.get("/api/account/sessions").json() assert len(listed) == 2 and sum(s["current"] for s in listed) == 1 assert pg_client.post("/api/account/sessions/signout-others").json() == {"revoked": 1} assert other.get("/api/auth/me").json()["code"] == "no_session" assert pg_client.get("/api/auth/me").status_code == 200 log = pg_client.get("/api/account/access-log").json() assert log[0]["action"] == "auth.signout_others" and log[0]["detail"] == {"count": 1} and log[0]["ok"] is True