Admin: Departments (name and stations; add shows the three groups IT must create), Profiles (welder stamp, station limits, active) with a CSV import that previews and applies only when clean, Checklists (weld and plain), Security (session, lockout and rate-limit settings with bounds, alerts for the last 24 hours, the integrity result), Access log with filters and a guarded CSV, Who is in, Stamp check, Notifications, and the employeeID flags. Every Admin change is audited. Worker: ad-reconcile (nightly and on "refresh from AD now"), session-refresh (15 minutes), integrity (seals, asset hashes, missing and orphan files, the audit chain), backup-check, notify-send and notify-digest. Notifications route per event type to AD groups, users, mailboxes or the department Supervisor group, immediately or in a daily digest, over STARTTLS verified against the PESCO CA, with dry run on the test stack. Backups: scripts/backup/ps-backup.sh and ps-restore.sh with a systemd timer at 02:00 America/Denver. Restore rehearsed on the test stack 2026-10-21. CI: pip-audit and npm audit. pypdf 6.21.1 and certifi 2026.10.19. Migration 0010. Closes the app part of I6, M3, O1, O2 and S3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
98 lines
4.5 KiB
Python
98 lines
4.5 KiB
Python
"""The session-refresh job: roles rebuilt for live sessions, disabled, gone and roleless users signed out."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Any
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
from fakedir import FakeDirectory
|
|
from pshelpers import app_conn, needs_pg
|
|
|
|
from packetstation.auth import directory as userdir
|
|
from packetstation.auth import sessions
|
|
from packetstation.feed.config import FeedSettings
|
|
from packetstation.settings import Settings
|
|
from packetstation.worker import runs
|
|
from packetstation.worker.jobs import ad_reconcile, session_refresh
|
|
from packetstation.worker.scheduler import Scheduler
|
|
|
|
|
|
def run(settings: Settings, d: FakeDirectory, name: str = "session-refresh") -> dict[str, Any]:
|
|
spec = session_refresh.SPEC if name == "session-refresh" else ad_reconcile.SPEC
|
|
sch = Scheduler(settings, build="pytest", jobs={name: spec})
|
|
sch.stack.cache["directory"] = d
|
|
return runs.run_job(sch, name, "schedule")
|
|
|
|
|
|
def session_for(settings: Settings, conn: Any, user_id: uuid.UUID, roles: list[str]) -> uuid.UUID:
|
|
_, row = sessions.create(
|
|
conn, settings, settings.session_key(), user_id=user_id, kind="password", roles=roles, groups=[]
|
|
)
|
|
conn.execute("UPDATE sessions SET roles_checked_at = now() - interval '1 hour' WHERE id = %s", (row["id"],))
|
|
sid: uuid.UUID = row["id"]
|
|
return sid
|
|
|
|
|
|
def test_cadence_follows_the_last_run(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
now = datetime(2026, 10, 21, 10, tzinfo=ZoneInfo("America/Denver"))
|
|
monkeypatch.setattr(session_refresh, "_minutes", 15)
|
|
assert session_refresh.cadence(FeedSettings(), now) == 900
|
|
session_refresh._set_minutes(30)
|
|
assert session_refresh.cadence(FeedSettings(), now) == 1800
|
|
|
|
|
|
@needs_pg
|
|
def test_session_refresh(pg_settings: Settings, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
d = FakeDirectory()
|
|
monkeypatch.setattr("packetstation.worker.jobs.raise_event", lambda *a, **k: None)
|
|
monkeypatch.setattr("packetstation.worker.jobs.resolve_event", lambda *a, **k: None)
|
|
assert run(pg_settings, d, "ad-reconcile")["status"] == "ok"
|
|
|
|
# Nobody signed in: nothing to do
|
|
row = run(pg_settings, d)
|
|
assert row["status"] == "ok" and row["counts"]["users"] == 0
|
|
|
|
g = {s: d.user(s).guid for s in ("clerk.clara", "wh.wade", "qc.quinn", "lead.lou", "sup.wes")}
|
|
with app_conn(pg_settings) as conn:
|
|
sid = {s: session_for(pg_settings, conn, uid, ["clerical"]) for s, uid in g.items()}
|
|
conn.execute("UPDATE sessions SET revoked_at = now() WHERE id = %s", (sid["sup.wes"],))
|
|
|
|
d.user("lead.lou").groups = ["PS-03-WeldParts-Supervisor"] # promoted
|
|
d.user("clerk.clara").disabled = True
|
|
d.users.remove(d.user("wh.wade"))
|
|
d.user("qc.quinn").groups = []
|
|
|
|
# AD down: degraded, nothing ended or changed
|
|
d.down = True
|
|
row = run(pg_settings, d)
|
|
assert row["status"] == "degraded" and row["counts"]["ad"] == "unavailable"
|
|
with app_conn(pg_settings) as conn:
|
|
assert conn.execute("SELECT count(*) FROM sessions WHERE revoked_at IS NULL").fetchone()[0] == 4
|
|
d.down = False
|
|
|
|
row = run(pg_settings, d)
|
|
assert row["status"] == "ok", row["errors"]
|
|
c = row["counts"]
|
|
assert c["users"] == 4 and c["refreshed"] == 1 and c["deactivated"] == 2 and c["roles_lost"] == 1
|
|
assert c["sessions_ended"] == 3 and c["roles_changed"] == 2 # Lou promoted, Quinn lost QC
|
|
with app_conn(pg_settings) as conn:
|
|
state = dict(conn.execute("SELECT id, revoked_reason FROM sessions").fetchall())
|
|
assert state[sid["clerk.clara"]] == "ad_disabled" and state[sid["wh.wade"]] == "ad_gone"
|
|
assert state[sid["qc.quinn"]] == "roles_lost" and state[sid["lead.lou"]] is None
|
|
snap, fresh = conn.execute(
|
|
"SELECT roles_snapshot, roles_checked_at > now() - interval '1 minute' FROM sessions WHERE id = %s",
|
|
(sid["lead.lou"],),
|
|
).fetchone()
|
|
assert snap["roles"] == ["shop:03:supervisor"] and snap["groups"] == ["PS-03-WeldParts-Supervisor"] and fresh
|
|
assert userdir.roles_of(conn, g["lead.lou"]) == ["shop:03:supervisor"]
|
|
active = dict(conn.execute("SELECT id, active FROM users").fetchall())
|
|
assert active[g["clerk.clara"]] is False and active[g["wh.wade"]] is False and active[g["qc.quinn"]] is True
|
|
ends = conn.execute(
|
|
"SELECT detail->>'reason' FROM audit_events WHERE action = 'auth.session_ended'"
|
|
" AND detail->>'by' = 'session-refresh' ORDER BY 1"
|
|
).fetchall()
|
|
assert [e[0] for e in ends] == ["ad_disabled", "ad_gone", "roles_lost"]
|