Profile: read-only name for cn-named directory entries

slapd refuses to remove an RDN value, and every legacy account is
cn=<name>,ou=People, so a name edit there always failed. Those accounts
now see their name read-only (the view rejects the profile action and
update_profile refuses as a backstop); uid-named accounts from website
registration can still edit it. README documents the cn/mail self-write
ACL and mail uniqueness overlay now applied in production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 22:05:29 -05:00
co-authored by Claude Opus 5.5
parent d73f7d1b80
commit 86ce546a17
5 changed files with 69 additions and 3 deletions
+21
View File
@@ -16,6 +16,7 @@ from ldap3.core.exceptions import LDAPException
from ldap3.operation.search import compile_filter, parse_filter
from ldap3.protocol.controls import build_control
from ldap3.utils.conv import escape_filter_chars
from ldap3.utils.dn import parse_dn
from . import directory
from .models import Account
@@ -83,6 +84,24 @@ def read_profile(account):
return values
def name_editable(dn):
"""False when cn names the entry: slapd refuses to remove an RDN value.
Legacy accounts are cn=<name>,ou=People; renaming them would change DNs
that other services store, so their name is shown read-only instead.
"""
try:
components = parse_dn(dn)
except Exception:
return False
for attribute, _value, separator in components:
if attribute.casefold() in {"cn", "2.5.4.3"}:
return False
if separator != "+":
return True
return False
def revision(account, values):
return signing.dumps({"account": str(account.pk), "uuid": account.ldap_uuid, "digest": _digest(values)}, salt="ldap-profile")
@@ -118,6 +137,8 @@ def update_profile(account, cleaned, expected_revision):
with _authorized(account, cleaned["current_password"]) as (conn, values):
if proof != {"account": str(account.pk), "uuid": account.ldap_uuid, "digest": _digest(values)}:
raise ValueError("Your profile changed. Reload it before saving.")
if not name_editable(account.ldap_dn):
raise ValueError("This account's name is part of its directory entry name.")
changes = {}
for name in TEXT_ATTRIBUTES:
new_values = cleaned[name]