"""The directory additions and the ad-reconcile job against the CI Samba domain controller (deploy/samba). Skipped unless the TEST_LDAP_* values are set, like test_ldap_dc.py. The seed leaves PS-14-Electrical-Lead out, adds PS-Extra-Legacy, nests Shop-Welders-Nested into PS-10-WeldAssembly-Worker, disables disabled.dan, gives dup.dave and dup.dina the same employeeID and noemp.nora none. Nothing here writes to the directory. """ from __future__ import annotations import uuid from pathlib import Path from typing import Any import pytest from pshelpers import app_conn, make_settings, needs_pg from test_ldap_dc import MISSING, ldap_values from packetstation.auth import directory as userdir from packetstation.auth import groups, sessions from packetstation.auth.ldap import AdUser, Directory from packetstation.settings import Settings from packetstation.worker import jobs, runs from packetstation.worker.jobs import ad_reconcile from packetstation.worker.scheduler import Scheduler pytestmark = pytest.mark.skipif(bool(MISSING), reason=f"no test domain controller ({', '.join(MISSING)} not set)") @pytest.fixture def directory(tmp_path: Path) -> Directory: return Directory(make_settings(tmp_path, **ldap_values())) def test_list_groups_detail(directory: Directory) -> None: detail = directory.list_groups_detail() names = {n.lower() for n, _ in detail} assert "ps-14-electrical-lead" not in names and "ps-extra-legacy" in names assert sum(1 for n in groups.group_names() if n.lower() in names) == 49 base = directory.settings.ldap_group_base_dn.lower() assert all(dn.lower().endswith(base) and dn.lower().startswith(f"cn={n.lower()},") for n, dn in detail) def test_members_of_dn_nested_and_disabled(directory: Directory) -> None: dn = dict(directory.list_groups_detail()) assert {u.sam_account for u in directory.members_of_dn(dn["PS-10-WeldAssembly-Worker"])} == {"nested.ned"} weld = {u.sam_account: u for u in directory.members_of_dn(dn["PS-03-WeldParts-Worker"])} assert {"weld.andy", "both.bo", "dup.dave", "disabled.dan"} <= set(weld) assert weld["disabled.dan"].disabled is True and weld["weld.andy"].disabled is False assert directory.members_of_dn("CN=PS-No-Such-Group," + directory.settings.ldap_group_base_dn) == [] def test_search_groups_and_users_by_guid(directory: Directory) -> None: found = directory.search_groups("Welders-Nest") assert [g["name"] for g in found] == ["Shop-Welders-Nested"] assert found[0]["description"] and found[0]["dn"].lower().endswith(directory.settings.ldap_base_dn.lower()) assert len(directory.search_groups("PS-", limit=3)) == 3 ann = directory.find_user("admin.ann") assert ann is not None ghost = uuid.uuid4() got = directory.users_by_guid([ann.guid, ghost]) assert got[ghost] is None and got[ann.guid] is not None and got[ann.guid].sam_account == "admin.ann" def _run(settings: Settings, d: Directory) -> dict[str, Any]: sch = Scheduler(settings, build="pytest", jobs={"ad-reconcile": ad_reconcile.SPEC}) sch.stack.cache["directory"] = d return runs.run_job(sch, "ad-reconcile", "schedule") def _session(settings: Settings, conn: Any, user_id: uuid.UUID) -> uuid.UUID: _, row = sessions.create( conn, settings, settings.session_key(), user_id=user_id, kind="password", roles=["clerical"], groups=[] ) sid: uuid.UUID = row["id"] return sid @needs_pg def test_reconcile_against_dc(pg_settings: Settings, directory: Directory, monkeypatch: pytest.MonkeyPatch) -> None: raised: list[tuple[str, dict[str, Any]]] = [] monkeypatch.setattr(jobs, "raise_event", lambda conn, t, **kw: raised.append((t, kw))) monkeypatch.setattr(jobs, "resolve_event", lambda conn, t, **kw: None) row = _run(pg_settings, directory) assert row["status"] == "ok", row["errors"] c = row["counts"] assert c["groups_missing"] == ["PS-14-Electrical-Lead"] and c["groups_extra"] == ["PS-Extra-Legacy"] assert c["groups_checked"] == 49 and c["users_seen"] == 16 assert "noemp.nora" in c["emp_missing"] and c["emp_duplicate"] == ["9001"] assert ("group_missing", "PS-14-Electrical-Lead") in [(t, kw["subject"]) for t, kw in raised] assert any(t == "ad_reconcile_report" for t, _ in raised) dan = directory.find_user("disabled.dan") nick = directory.find_user("norole.nick") ned = directory.find_user("nested.ned") assert dan is not None and nick is not None and ned is not None ghost = AdUser(uuid.uuid4(), "CN=ghost,DC=pstest,DC=local", "ghost.gary", None, "Gary Ghost", "9999", None, False) with app_conn(pg_settings) as conn: flags = dict(conn.execute("SELECT sam_account, directory_flags FROM users").fetchall()) assert flags["dup.dave"]["emp_duplicate"] and flags["dup.dina"]["emp_duplicate"] assert flags["noemp.nora"]["emp_missing"] and flags["disabled.dan"]["disabled"] assert userdir.roles_of(conn, ned.guid) == ["shop:10:worker"] assert conn.execute("SELECT active FROM users WHERE id = %s", (dan.guid,)).fetchone()[0] is False # A disabled account that kept a session, an account in no group, an account AD no longer has userdir.upsert_user(conn, nick, groups=[]) userdir.upsert_user(conn, ghost, groups=[]) sid = {u: _session(pg_settings, conn, u) for u in (dan.guid, nick.guid, ghost.guid)} row = _run(pg_settings, directory) assert row["status"] == "ok", row["errors"] assert row["counts"]["sessions_ended"] == 3 and row["counts"]["deactivated"] == 1 # the ghost with app_conn(pg_settings) as conn: why = dict(conn.execute("SELECT id, revoked_reason FROM sessions").fetchall()) assert why[sid[dan.guid]] == "ad_disabled" and why[sid[nick.guid]] == "roles_lost" assert why[sid[ghost.guid]] == "ad_gone" gary = conn.execute("SELECT active, directory_flags FROM users WHERE id = %s", (ghost.guid,)).fetchone() assert gary[0] is False and gary[1]["gone"] is True assert conn.execute("SELECT active FROM users WHERE id = %s", (nick.guid,)).fetchone()[0] is True