Files

204 lines
10 KiB
Python
Raw Permalink Normal View History

"""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