From 95291df971b8c80420ca57793e4ba0ab7590b9f9 Mon Sep 17 00:00:00 2001 From: B0rbor4d Date: Mon, 28 Sep 2026 22:16:53 +0000 Subject: [PATCH] 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. --- README.md | 3 ++- backend/app/services/ldap_auth.py | 21 +++++++++++++-------- backend/tests/test_ldap_auth.py | 5 ++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8c6dde4..9203e72 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ WinRM/SSH-Config ist in den Satellite gewandert (dessen `config.yaml`). Dashboard-Login per AD (User-Bind, kein Servicekonto): Der User bindet mit seinen eigenen Credentials als `user@domain`, die Gruppenmitgliedschaft wird -ueber die Matching-Rule-In-Chain geprueft (verschachtelte Gruppen aufloesend). +ueber das `memberOf`-Attribut geprueft (direkte Mitgliedschaft; die +Matching-Rule-In-Chain funktioniert gegen die insight.local-DCs nicht). Nur Mitglieder von `LDAP_ADMIN_GROUP` bekommen Zugang. Lokal angelegte User (Initial-Admin) bleiben als Fallback wirksam. diff --git a/backend/app/services/ldap_auth.py b/backend/app/services/ldap_auth.py index db2cf7b..8c8c8cb 100644 --- a/backend/app/services/ldap_auth.py +++ b/backend/app/services/ldap_auth.py @@ -4,11 +4,16 @@ User-bind design (no service account required): 1. The user binds with their own credentials as ``user@domain`` (AD accepts userPrincipalName as bind DN). 2. Over that authenticated connection we search the user's own directory - entry, restricted by the in-chain matching rule to members of the - configured admin group (direct or nested membership). + entry and verify direct membership in the configured admin group via + the ``memberOf`` attribute. Only members of the configured group may log in. All other users are rejected before any local session is created. + +Note: nested group membership is intentionally not resolved. The +Matching-Rule-In-Chain (1.2.840.113556.1941) was tested against the +insight.local DCs and returned no matches at all, even for direct +members with ASCII-only group DNs - so we check ``memberOf`` directly. """ import ssl @@ -20,9 +25,6 @@ from ldap3.utils.conv import escape_filter_chars from app.core.config import Settings -# LDAP_MATCHING_RULE_IN_CHAIN: resolves nested group membership server-side -_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1941" - class LdapAuthError(Exception): """Authentication failed. Message is safe to show to the user.""" @@ -89,9 +91,12 @@ def authenticate(username: str, password: str, settings: Settings) -> LdapUser: user_filter = settings.ldap_user_filter.format( username=escape_filter_chars(username.strip()) ) - # One search proves: user exists AND is (transitively) in the admin - # group. Non-members simply match nothing. - group_filter = f"(memberOf:{_MATCHING_RULE_IN_CHAIN}:={settings.ldap_admin_group})" + # One search proves: user exists AND is a direct member of the + # configured admin group. Non-members simply match nothing. + # (memberOf is the backlink of the group's member attribute; the + # matching-rule-in-chain was rejected because it does not work + # against these DCs, see module docstring.) + group_filter = f"(memberOf={settings.ldap_admin_group})" ok = conn.search( search_base=settings.ldap_user_search_base, search_filter=f"(&{user_filter}{group_filter})", diff --git a/backend/tests/test_ldap_auth.py b/backend/tests/test_ldap_auth.py index 817a02c..8072e74 100644 --- a/backend/tests/test_ldap_auth.py +++ b/backend/tests/test_ldap_auth.py @@ -98,10 +98,9 @@ def test_success_returns_admin_user(monkeypatch: pytest.MonkeyPatch) -> None: 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 in-chain rule + # search filter proves admin-group membership via memberOf search_kwargs = conn.search.call_args.kwargs - assert "memberOf:1.2.840.113556.1941:=" in search_kwargs["search_filter"] - assert "CN=Domain Admins,CN=Users,DC=insight,DC=local" in search_kwargs["search_filter"] + 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"