Files

134 lines
5.3 KiB
Python
Raw Permalink Normal View History

"""Against the CI Samba domain controller (deploy/samba). Skipped unless the TEST_LDAP_* values are set.
TEST_LDAP_URL, TEST_LDAP_BASE_DN, TEST_LDAP_BIND_DN, TEST_LDAP_PASSWORD_FILE, TEST_LDAP_GROUP_BASE_DN,
TEST_LDAP_CA_BUNDLE and TEST_DC_USERS_FILE (the users.json the DC seed writes, with per-run passwords).
"""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
import pytest
from fastapi.testclient import TestClient
from pshelpers import ORIGIN_HEADERS, app_conn, make_settings, needs_pg
from packetstation.auth import groups
from packetstation.auth.directory import refresh_from_ad
from packetstation.auth.ldap import Directory
from packetstation.settings import Settings
ENV = (
"TEST_LDAP_URL",
"TEST_LDAP_BASE_DN",
"TEST_LDAP_BIND_DN",
"TEST_LDAP_PASSWORD_FILE",
"TEST_LDAP_GROUP_BASE_DN",
"TEST_LDAP_CA_BUNDLE",
"TEST_DC_USERS_FILE",
)
MISSING = [k for k in ENV if not os.environ.get(k)]
pytestmark = pytest.mark.skipif(bool(MISSING), reason=f"no test domain controller ({', '.join(MISSING)} not set)")
def ldap_values() -> dict[str, Any]:
return {
"ldap_url": os.environ["TEST_LDAP_URL"],
"ldap_base_dn": os.environ["TEST_LDAP_BASE_DN"],
"ldap_bind_dn": os.environ["TEST_LDAP_BIND_DN"],
"ldap_password_file": Path(os.environ["TEST_LDAP_PASSWORD_FILE"]),
"ldap_group_base_dn": os.environ["TEST_LDAP_GROUP_BASE_DN"],
"ldap_ca_bundle": Path(os.environ["TEST_LDAP_CA_BUNDLE"]),
"ldap_starttls": os.environ.get("TEST_LDAP_STARTTLS", "").lower() == "true",
}
@pytest.fixture(scope="module")
def dc_users() -> dict[str, dict[str, str]]:
data = json.loads(Path(os.environ["TEST_DC_USERS_FILE"]).read_text(encoding="utf-8"))
users: dict[str, dict[str, str]] = data["users"]
return users
@pytest.fixture
def directory(tmp_path: Path) -> Directory:
return Directory(make_settings(tmp_path, **ldap_values()))
def roles_of(d: Directory, login: str) -> list[str]:
u = d.find_user(login)
assert u is not None, login
return groups.roles_from_groups(d.groups_of(u.dn))
def test_password_bind(directory: Directory, dc_users: dict[str, dict[str, str]]) -> None:
u = directory.find_user("clerk.clara")
assert u is not None and not u.disabled and u.emp_no == "4001" and u.display_name == "Clara Clerk"
assert directory.verify_password(u.dn, dc_users["clerk.clara"]["password"]) is True
assert directory.verify_password(u.dn, "not-the-password") is False
assert directory.verify_password(u.dn, "") is False
assert roles_of(directory, "clerk.clara") == ["clerical"]
again = directory.get_user_by_guid(u.guid)
assert again is not None and again.sam_account == "clerk.clara"
def test_nested_and_levels(directory: Directory) -> None:
assert roles_of(directory, "nested.ned") == ["shop:10:worker"]
assert roles_of(directory, "both.bo") == ["shop:03:supervisor"]
assert roles_of(directory, "multi.mia") == ["clerical", "shop:06:supervisor"]
assert roles_of(directory, "norole.nick") == []
def test_disabled_account(directory: Directory, dc_users: dict[str, dict[str, str]]) -> None:
u = directory.find_user("disabled.dan")
assert u is not None and u.disabled is True
assert directory.verify_password(u.dn, dc_users["disabled.dan"]["password"]) is False
def test_group_listing_and_members(directory: Directory) -> None:
found = directory.list_groups()
lower = {g.lower() for g in found}
assert "ps-14-electrical-lead" not in lower and "ps-extra-legacy" in lower
assert sum(1 for n in groups.group_names() if n.lower() in lower) == 49
sams = {u.sam_account for u in directory.members_of("PS-03-WeldParts-Worker")}
assert {"weld.andy", "both.bo", "dup.dave"} <= sams
assert directory.members_of("PS-No-Such-Group") == []
@needs_pg
def test_refresh_flags_duplicates(pg_settings: Settings, directory: Directory) -> None:
with app_conn(pg_settings) as conn:
res = refresh_from_ad(conn, directory)
assert res.groups_missing == ["PS-14-Electrical-Lead"]
assert "9001" in res.emp_duplicate and "noemp.nora" in res.emp_missing
@needs_pg
def test_signin_api_against_dc(
pg_settings: Settings, tmp_path: Path, dev_visual: None, dc_users: dict[str, dict[str, str]]
) -> None:
from packetstation.main import create_app
s = make_settings(
tmp_path / "dc",
**ldap_values(),
**{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,
)
with TestClient(create_app(s), headers=ORIGIN_HEADERS) as c:
def signin(sam: str, pw: str | None = None) -> Any:
password = dc_users[sam]["password"] if pw is None else pw
return c.post("/api/auth/signin/password", json={"login": sam, "password": password})
r = signin("clerk.clara")
assert r.status_code == 200 and r.json()["roles"] == ["clerical"]
assert signin("clerk.clara", "wrong").status_code == 401
assert signin("disabled.dan").status_code == 401
r = signin("norole.nick")
assert r.status_code == 403 and r.json()["code"] == "no_role"
assert signin("nested.ned").json()["roles"] == ["shop:10:worker"]