134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
"""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"]
|