Profile: only edit fields Dex reads back

The Dex LDAP connector publishes uid, cn and mail as claims, so the
profile page now offers just Name (cn), the verified email change and
the password. The photo endpoint, SSH keys, addresses, phones and
other extended attributes are no longer read or written; existing LDAP
values are left untouched. Apache's profile upload limit drops back to
the 256 KiB used by other member routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 21:42:57 -05:00
co-authored by Claude Opus 5.5
parent 67d8caccce
commit d73f7d1b80
9 changed files with 114 additions and 507 deletions
+9 -21
View File
@@ -6,13 +6,12 @@ No service-account writes, password hashes, tokens or profile snapshots are stor
import hashlib
import json
import re
from contextlib import contextmanager
from django.conf import settings
from django.core import signing
from django.db import transaction
from ldap3 import BASE, MODIFY_ADD, MODIFY_REPLACE
from ldap3 import BASE, MODIFY_REPLACE
from ldap3.core.exceptions import LDAPException
from ldap3.operation.search import compile_filter, parse_filter
from ldap3.protocol.controls import build_control
@@ -22,12 +21,12 @@ from . import directory
from .models import Account
TEXT_ATTRIBUTES = (
"description", "displayName", "givenName", "l", "labeledURI", "mobile", "o",
"postalAddress", "postalCode", "roomNumber", "sn", "sshPublicKey", "st",
"street", "telephoneNumber", "title",
)
READ_ATTRIBUTES = (*TEXT_ATTRIBUTES, "mail", "jpegPhoto", "entryUUID", "uid", "modifyTimestamp", "objectClass")
# Only attributes the Dex LDAP connector reads back into ID-token claims are
# editable: cn (nameAttr -> "name"). mail ("email") is changed through the
# separate verified flow and uid ("preferred_username") is immutable. Other
# directory attributes are left untouched, neither shown nor cleared.
TEXT_ATTRIBUTES = ("cn",)
READ_ATTRIBUTES = (*TEXT_ATTRIBUTES, "mail", "entryUUID", "uid", "modifyTimestamp")
def _values(entry):
@@ -89,10 +88,8 @@ def revision(account, values):
def initial(values):
def postal(value):
# Decode once: a literal '\24' represented by '\5c24' must not become '$'.
return re.sub(r"\\(24|5[cC])", lambda match: "$" if match.group(1) == "24" else "\\", str(value).replace("$", "\n"))
return {name: ("\n\n".join(postal(value) for value in values[name]) if name == "postalAddress" else "\n".join(str(value) for value in values[name])) for name in TEXT_ATTRIBUTES}
# Dex publishes the first cn value; saving collapses any extras to one.
return {name: str(values[name][0]) if values[name] else "" for name in TEXT_ATTRIBUTES}
@contextmanager
@@ -126,15 +123,6 @@ def update_profile(account, cleaned, expected_revision):
new_values = cleaned[name]
if new_values != values[name]:
changes[name] = [(MODIFY_REPLACE, new_values)]
if cleaned["sshPublicKey"] and "ldappublickey" not in {str(value).casefold() for value in values["objectClass"]}:
# Fixed auxiliary class only, never arbitrary browser-supplied
# schema/classes. Install the SSH schema before enabling edits.
changes["objectClass"] = [(MODIFY_ADD, ["ldapPublicKey"])]
photo = cleaned.get("jpegPhoto")
if photo is not None:
changes["jpegPhoto"] = [(MODIFY_REPLACE, [photo])]
elif cleaned.get("remove_photo") and values["jpegPhoto"]:
changes["jpegPhoto"] = [(MODIFY_REPLACE, [])]
if changes and not conn.modify(account.ldap_dn, changes, controls=[_assertion(values)]):
raise LDAPException("Profile modification rejected.")