88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
"""LDAP helpers and the fake directory; no domain controller needed."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import uuid
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from fakedir import FAKE_PASSWORD, FakeDirectory
|
||
|
|
from pshelpers import make_settings
|
||
|
|
|
||
|
|
from packetstation.auth import groups
|
||
|
|
from packetstation.auth.ldap import Directory, LdapUnavailable, guid_filter, parse_guid, user_from_entry
|
||
|
|
|
||
|
|
GUID = uuid.UUID("01234567-89ab-cdef-0123-456789abcdef")
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_guid_bytes_and_string() -> None:
|
||
|
|
raw = GUID.bytes_le
|
||
|
|
assert parse_guid(raw) == GUID
|
||
|
|
assert parse_guid([raw]) == GUID
|
||
|
|
assert parse_guid("{01234567-89ab-cdef-0123-456789abcdef}") == GUID
|
||
|
|
assert parse_guid("01234567-89AB-CDEF-0123-456789ABCDEF") == GUID
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
parse_guid(b"short")
|
||
|
|
|
||
|
|
|
||
|
|
def test_guid_filter_escapes_every_byte() -> None:
|
||
|
|
flt = guid_filter(GUID)
|
||
|
|
assert flt.startswith("(objectGUID=\\67\\45\\23\\01") and flt.endswith(")")
|
||
|
|
assert flt.count("\\") == 16
|
||
|
|
|
||
|
|
|
||
|
|
def test_user_from_entry() -> None:
|
||
|
|
entry = {
|
||
|
|
"dn": "CN=Andy,DC=x",
|
||
|
|
"raw_attributes": {"objectGUID": [GUID.bytes_le]},
|
||
|
|
"attributes": {
|
||
|
|
"sAMAccountName": ["weld.andy"],
|
||
|
|
"userPrincipalName": ["[email protected]"],
|
||
|
|
"displayName": ["Andy Welder"],
|
||
|
|
"employeeID": ["0303"],
|
||
|
|
"mail": [],
|
||
|
|
"userAccountControl": ["514"],
|
||
|
|
"distinguishedName": ["CN=Andy,DC=x"],
|
||
|
|
},
|
||
|
|
}
|
||
|
|
u = user_from_entry(entry)
|
||
|
|
assert u.guid == GUID and u.sam_account == "weld.andy" and u.display_name == "Andy Welder"
|
||
|
|
assert u.emp_no == "0303" # text, leading zero kept
|
||
|
|
assert u.email is None and u.disabled is True and u.dn == "CN=Andy,DC=x"
|
||
|
|
|
||
|
|
|
||
|
|
def test_plain_ldap_refused_without_starttls(tmp_path: Path) -> None:
|
||
|
|
pw = tmp_path / "pw"
|
||
|
|
pw.write_text("x")
|
||
|
|
s = make_settings(tmp_path, ldap_url="ldap://dc.example:389", ldap_bind_dn="CN=svc", ldap_password_file=pw)
|
||
|
|
with pytest.raises(LdapUnavailable, match="plain LDAP"):
|
||
|
|
Directory(s).find_user("someone")
|
||
|
|
|
||
|
|
|
||
|
|
def test_empty_password_never_binds(tmp_path: Path) -> None:
|
||
|
|
s = make_settings(tmp_path, ldap_url="ldaps://dc.invalid:636")
|
||
|
|
assert Directory(s).verify_password("CN=x,DC=y", "") is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_service_secret_is_unavailable(tmp_path: Path) -> None:
|
||
|
|
s = make_settings(
|
||
|
|
tmp_path, ldap_url="ldaps://dc.invalid:636", ldap_bind_dn="CN=svc", ldap_password_file=tmp_path / "none"
|
||
|
|
)
|
||
|
|
with pytest.raises(LdapUnavailable, match="LDAP_PASSWORD_FILE"):
|
||
|
|
Directory(s).list_groups()
|
||
|
|
|
||
|
|
|
||
|
|
def test_fake_directory_nested_and_ou() -> None:
|
||
|
|
d = FakeDirectory()
|
||
|
|
ned = d.find_user("NESTED.NED")
|
||
|
|
assert ned is not None
|
||
|
|
assert groups.roles_from_groups(d.groups_of(ned.dn)) == ["shop:10:worker"]
|
||
|
|
bo = d.find_user("[email protected]")
|
||
|
|
assert bo is not None
|
||
|
|
assert groups.roles_from_groups(d.groups_of(bo.dn)) == ["shop:03:supervisor"]
|
||
|
|
assert "PS-14-Electrical-Lead" not in d.list_groups() and "PS-Extra-Legacy" in d.list_groups()
|
||
|
|
sams = {u.sam_account for u in d.members_of("PS-03-WeldParts-Worker")}
|
||
|
|
assert {"weld.andy", "both.bo", "dup.dave", "disabled.dan"} <= sams
|
||
|
|
assert d.verify_password(bo.dn, FAKE_PASSWORD) and not d.verify_password(bo.dn, "nope")
|
||
|
|
assert d.verify_password(bo.dn, "") is False
|