Login
This commit is contained in:
@@ -8,10 +8,16 @@ from urllib.parse import urlsplit
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from ldap3 import BASE, MODIFY_ADD, MODIFY_DELETE, Connection, Server, Tls
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from ldap3.utils.dn import escape_rdn
|
||||
|
||||
from .models import Account
|
||||
|
||||
|
||||
class LDAPServiceBindError(LDAPException):
|
||||
"""The application's LDAP service credentials were rejected, not a user's."""
|
||||
|
||||
|
||||
@contextmanager
|
||||
def connection():
|
||||
@@ -25,7 +31,12 @@ def connection():
|
||||
# Plaintext requires explicit operator consent; LDAPS is never downgraded.
|
||||
if url.scheme == "ldap" and not settings.LDAP_ALLOW_INSECURE and not conn.start_tls():
|
||||
raise LDAPException("StartTLS failed; refusing to bind.")
|
||||
if not conn.bind():
|
||||
try:
|
||||
bound = conn.bind()
|
||||
except LDAPInvalidCredentialsResult:
|
||||
# Do not expose server diagnostics or retry with a user's password.
|
||||
raise LDAPServiceBindError("Check LDAP_BIND_DN and LDAP_BIND_PASSWORD: use the service account's plaintext password, not its stored hash.") from None
|
||||
if not bound:
|
||||
raise LDAPException("LDAP bind failed.")
|
||||
yield conn
|
||||
finally:
|
||||
@@ -41,16 +52,6 @@ def _identity(entry):
|
||||
return {"uid": str(entry.uid.value), "email": str(entry.mail.value), "ldap_dn": entry.entry_dn, "ldap_uuid": str(entry.entryUUID.value)}
|
||||
|
||||
|
||||
def identity_for_login(uid):
|
||||
if not isinstance(uid, str) or not re.fullmatch(r"[a-z][a-z0-9._-]{2,47}", uid):
|
||||
raise ValueError("Dex must return a valid LDAP uid as preferred_username.")
|
||||
with connection() as conn:
|
||||
entries = _find(conn, f"(&(objectClass=inetOrgPerson)(uid={escape_filter_chars(uid)}))")
|
||||
if len(entries) != 1:
|
||||
raise ValueError("LDAP identity is missing or ambiguous.")
|
||||
return _identity(entries[0])
|
||||
|
||||
|
||||
def create_account(uid, email, password):
|
||||
if settings.LDAP_PASSWORD_SCHEME != "{ARGON2}":
|
||||
raise ImproperlyConfigured("Verify slapd's Argon2 password policy before enabling registration.")
|
||||
@@ -82,9 +83,22 @@ def reconcile_groups(account, desired, managed):
|
||||
if not placeholder:
|
||||
raise ImproperlyConfigured("LDAP_GROUP_PLACEHOLDER_DN is required to revoke the final member.")
|
||||
with connection() as conn:
|
||||
entries = _find(conn, f"(entryUUID={escape_filter_chars(account.ldap_uuid)})")
|
||||
if account.ldap_uuid:
|
||||
query = f"(entryUUID={escape_filter_chars(account.ldap_uuid)})"
|
||||
else:
|
||||
# Only the worker resolves first-time Dex users. Never fall back to
|
||||
# username once an entryUUID has been pinned (including deletions).
|
||||
if account.oidc_issuer != settings.DEX_ISSUER or not account.oidc_sub or not re.fullmatch(r"[a-z][a-z0-9._-]{2,47}", account.uid):
|
||||
raise ValueError("A verified Dex mapping is required to resolve the directory identity.")
|
||||
query = f"(&(objectClass=inetOrgPerson)(uid={escape_filter_chars(account.uid)}))"
|
||||
entries = _find(conn, query)
|
||||
if len(entries) != 1:
|
||||
raise ValueError("Membership identity missing or ambiguous; refusing to modify groups.")
|
||||
identity = _identity(entries[0])
|
||||
if not identity["ldap_uuid"] or identity["ldap_uuid"] == "None" or len(identity["ldap_uuid"]) > 64:
|
||||
raise ValueError("A directory entryUUID is required to synchronize groups.")
|
||||
if not account.ldap_uuid and Account.objects.filter(ldap_uuid=identity["ldap_uuid"]).exclude(pk=account.pk).exists():
|
||||
raise ValueError("Directory entry is already pinned to another local account; refusing to modify groups.")
|
||||
dn = entries[0].entry_dn
|
||||
if placeholder.casefold() == dn.casefold():
|
||||
raise ImproperlyConfigured("The LDAP sentinel must not be a login account.")
|
||||
@@ -107,4 +121,6 @@ def reconcile_groups(account, desired, managed):
|
||||
changes.append((MODIFY_ADD, [dn]))
|
||||
if changes and not conn.modify(group_dn, {"uniqueMember": changes}):
|
||||
raise LDAPException("LDAP group synchronization failed.")
|
||||
# Caller holds the account row lock and persists this pin with sync state.
|
||||
account.ldap_uuid = identity["ldap_uuid"]
|
||||
return dn
|
||||
|
||||
Reference in New Issue
Block a user