Files
insight-updater/backend/tests/test_ldap_auth.py
T
B0rbor4d 95291df971 LDAP: Gruppenpruefung auf direktes memberOf umstellen
Matching-Rule-In-Chain liefert gegen die insight.local-DCs grundsaetzlich
keine Treffer (auch nicht bei ASCII-Gruppen und direkter Mitgliedschaft) -
live im Container verifiziert. Direkte memberOf-Pruefung funktioniert,
verschachtelte Gruppen werden bewusst nicht aufgeloest.
2026-09-28 22:16:53 +00:00

123 lines
4.4 KiB
Python

"""Unit tests for LDAP user-bind authentication (ldap3 fully mocked)."""
from unittest.mock import MagicMock
import pytest
from app.core.config import Settings
from app.services.ldap_auth import LdapAuthError, authenticate
def _settings(**overrides: object) -> Settings:
base: dict[str, object] = {
"ldap_enabled": True,
"ldap_uri": "ldaps://ads-server-01.insight.local:636",
"ldap_domain": "insight.local",
"ldap_user_search_base": "DC=insight,DC=local",
"ldap_user_filter": "(sAMAccountName={username})",
"ldap_admin_group": "CN=Domain Admins,CN=Users,DC=insight,DC=local",
"ldap_timeout_seconds": 10,
}
base.update(overrides)
return Settings(**base) # type: ignore[arg-type]
def _entry(full_name: str = "Flo Hartmann", email: str = "flo@example.com") -> MagicMock:
entry = MagicMock()
entry.displayName = full_name
entry.mail = email
return entry
def _patch_ldap(monkeypatch: pytest.MonkeyPatch, conn: MagicMock) -> None:
mock_ldap3 = MagicMock()
mock_ldap3.Server.return_value = MagicMock()
mock_ldap3.Connection.return_value = conn
mock_ldap3.SUBTREE = "SUBTREE"
mock_ldap3.NONE = "NO_INFO"
mock_ldap3.utils.conv.escape_filter_chars = lambda s: s
monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3)
def test_disabled_raises() -> None:
with pytest.raises(LdapAuthError, match="nicht aktiviert"):
authenticate("flo", "secret", _settings(ldap_enabled=False))
def test_missing_admin_group_raises() -> None:
with pytest.raises(LdapAuthError, match="nicht konfiguriert"):
authenticate("flo", "secret", _settings(ldap_admin_group=""))
def test_empty_password_raises() -> None:
with pytest.raises(LdapAuthError):
authenticate("flo", "", _settings())
def test_bind_failure_raises_generic(monkeypatch: pytest.MonkeyPatch) -> None:
from ldap3.core.exceptions import LDAPExceptionError
mock_ldap3 = MagicMock()
mock_ldap3.Server.return_value = MagicMock()
mock_ldap3.Connection.side_effect = LDAPExceptionError("invalidCredentials")
monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3)
with pytest.raises(LdapAuthError, match="Benutzername oder Passwort falsch"):
authenticate("flo", "wrong", _settings())
def test_search_without_match_denied(monkeypatch: pytest.MonkeyPatch) -> None:
conn = MagicMock()
conn.__enter__.return_value = conn
conn.search.return_value = True
conn.entries = []
_patch_ldap(monkeypatch, conn)
with pytest.raises(LdapAuthError, match="Kein Zugriff"):
authenticate("flo", "secret", _settings())
def test_success_returns_admin_user(monkeypatch: pytest.MonkeyPatch) -> None:
conn = MagicMock()
conn.__enter__.return_value = conn
conn.search.return_value = True
conn.entries = [_entry()]
mock_ldap3 = MagicMock()
mock_ldap3.Server.return_value = MagicMock()
mock_ldap3.Connection.return_value = conn
mock_ldap3.SUBTREE = "SUBTREE"
mock_ldap3.NONE = "NO_INFO"
mock_ldap3.utils.conv.escape_filter_chars = lambda s: s
monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3)
user = authenticate("flo", "secret", _settings())
assert user.username == "flo"
assert user.full_name == "Flo Hartmann"
assert user.email == "flo@example.com"
assert user.is_admin is True
# bind DN uses user@domain format
assert mock_ldap3.Connection.call_args.kwargs["user"] == "flo@insight.local"
# search filter proves admin-group membership via memberOf
search_kwargs = conn.search.call_args.kwargs
assert "(memberOf=CN=Domain Admins,CN=Users,DC=insight,DC=local)" in search_kwargs["search_filter"]
assert search_kwargs["search_base"] == "DC=insight,DC=local"
def test_optional_fields_none_when_empty(monkeypatch: pytest.MonkeyPatch) -> None:
conn = MagicMock()
conn.__enter__.return_value = conn
conn.search.return_value = True
conn.entries = [_entry(full_name="", email="")]
mock_ldap3 = MagicMock()
mock_ldap3.Server.return_value = MagicMock()
mock_ldap3.Connection.return_value = conn
mock_ldap3.SUBTREE = "SUBTREE"
mock_ldap3.NONE = "NO_INFO"
mock_ldap3.utils.conv.escape_filter_chars = lambda s: s
monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3)
user = authenticate("flo", "secret", _settings())
assert user.full_name is None
assert user.email is None