Files
PacketStation/tests/server/fakedir.py
T

141 lines
5.1 KiB
Python
Raw Normal View History

"""An in-memory DirectoryProtocol for tests. Passwords here are obviously fake fixture values."""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from packetstation.auth.groups import group_names
from packetstation.auth.ldap import AdUser, LdapUnavailable
FAKE_PASSWORD = "fake-password-1"
NESTED_GROUP = "Shop-Welders-Nested"
@dataclass
class FakeUser:
sam: str
display: str
emp_no: str | None
groups: list[str]
password: str = FAKE_PASSWORD
disabled: bool = False
guid: uuid.UUID = field(default_factory=uuid.uuid4)
@property
def dn(self) -> str:
return f"CN={self.display},OU=Users,OU=PESCO_FARMINGTON,DC=pstest,DC=local"
@property
def upn(self) -> str:
return f"{self.sam}@pstest.local"
def default_users() -> list[FakeUser]:
"""The same people as the CI domain controller (deploy/samba)."""
return [
FakeUser("admin.ann", "Ann Admin", "1001", ["PS-Admins"]),
FakeUser("clerk.clara", "Clara Clerk", "4001", ["PS-Clerical"]),
FakeUser("clerk.leo", "Leo Lead", "4002", ["PS-Clerical-Supervisor"]),
FakeUser("wh.wade", "Wade Warehouse", "6001", ["PS-Warehouse"]),
FakeUser("qc.quinn", "Quinn QC", "5001", ["PS-QC"]),
FakeUser("pm.paula", "Paula PM", "7001", ["PS-ProjectMgmt"]),
FakeUser("weld.andy", "Andy Welder", "3003", ["PS-03-WeldParts-Worker"]),
FakeUser("lead.lou", "Lou Lead", "3002", ["PS-03-WeldParts-Lead"]),
FakeUser("sup.wes", "Wes Super", "2003", ["PS-03-WeldParts-Supervisor"]),
FakeUser("multi.mia", "Mia Multi", "2006", ["PS-Clerical", "PS-06-CodeBldg2-Supervisor"]),
FakeUser("nested.ned", "Ned Nested", "3010", [NESTED_GROUP]),
FakeUser("both.bo", "Bo Both", "3011", ["PS-03-WeldParts-Worker", "PS-03-WeldParts-Supervisor"]),
FakeUser("disabled.dan", "Dan Disabled", "3004", ["PS-03-WeldParts-Worker"], disabled=True),
FakeUser("noemp.nora", "Nora NoEmp", None, ["PS-Warehouse"]),
FakeUser("dup.dave", "Dave Dup", "9001", ["PS-03-WeldParts-Worker"]),
FakeUser("dup.dina", "Dina Dup", "9001", ["PS-Warehouse"]),
FakeUser("norole.nick", "Nick NoRole", "8001", []),
]
class FakeDirectory:
"""Users, groups in the PacketStation OU, and nested groups outside it."""
def __init__(
self,
users: list[FakeUser] | None = None,
ou_groups: list[str] | None = None,
nested: dict[str, list[str]] | None = None,
):
self.users = users if users is not None else default_users()
if ou_groups is None:
ou_groups = [g for g in group_names() if g != "PS-14-Electrical-Lead"] + ["PS-Extra-Legacy"]
self.ou_groups = ou_groups
self.nested = nested if nested is not None else {NESTED_GROUP: ["PS-10-WeldAssembly-Worker"]}
self.down = False
self.binds = 0
self.searches = 0
# ------------------------------------------------ helpers
def user(self, sam: str) -> FakeUser:
return next(u for u in self.users if u.sam == sam)
def _check(self) -> None:
if self.down:
raise LdapUnavailable("fake directory is down")
self.searches += 1
def _ad(self, u: FakeUser) -> AdUser:
return AdUser(u.guid, u.dn, u.sam, u.upn, u.display, u.emp_no, f"{u.sam}@example.test", u.disabled)
def _closure(self, u: FakeUser) -> set[str]:
seen: set[str] = set()
todo = list(u.groups)
while todo:
g = todo.pop()
if g.lower() in seen:
continue
seen.add(g.lower())
todo.extend(self.nested.get(g, []))
return seen
def _in_ou(self, names: set[str]) -> list[str]:
return sorted(g for g in self.ou_groups if g.lower() in names)
# ------------------------------------------------ protocol
def find_user(self, login: str) -> AdUser | None:
self._check()
key = login.strip().lower()
for u in self.users:
if key in (u.sam.lower(), u.upn.lower()):
return self._ad(u)
return None
def get_user_by_guid(self, guid: uuid.UUID) -> AdUser | None:
self._check()
for u in self.users:
if u.guid == guid:
return self._ad(u)
return None
def verify_password(self, dn: str, password: str) -> bool:
if self.down:
raise LdapUnavailable("fake directory is down")
if not password:
return False
self.binds += 1
return any(u.dn == dn and u.password == password for u in self.users)
def groups_of(self, dn: str) -> list[str]:
self._check()
u = next((x for x in self.users if x.dn == dn), None)
return [] if u is None else self._in_ou(self._closure(u))
def list_groups(self) -> list[str]:
self._check()
return sorted(self.ou_groups)
def members_of(self, group_cn: str) -> list[AdUser]:
self._check()
if group_cn.lower() not in {g.lower() for g in self.ou_groups}:
return []
return [self._ad(u) for u in self.users if group_cn.lower() in self._closure(u)]