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
+31
View File
@@ -81,6 +81,17 @@ class ProfileFormTests(SimpleTestCase):
self.assertEqual(profiles.initial(profile_values(cn=["Alice", "Alice Example"])), {"cn": "Alice"})
self.assertEqual(profiles.initial(profile_values(cn=[])), {"cn": ""})
def test_name_editable_only_when_cn_does_not_name_the_entry(self):
for dn, editable in (
(ALICE_DN, True), ("uid=bob,ou=People,dc=eom,dc=dev", True),
("uid=bob,cn=Group,dc=eom,dc=dev", True),
("cn=Logan Fick,ou=People,dc=eom,dc=dev", False), ("CN=x,dc=eom,dc=dev", False),
("uid=bob+cn=Bob,ou=People,dc=eom,dc=dev", False), ("2.5.4.3=x,dc=eom,dc=dev", False),
("", False), ("not a dn", False),
):
with self.subTest(dn=dn):
self.assertIs(profiles.name_editable(dn), editable)
def test_field_groups_and_unique_password_ids(self):
form = self.form()
grouped = [*form.identity_fields(), *form.authorization_fields()]
@@ -481,6 +492,19 @@ class ProfileViewTests(TestCase):
self.account.refresh_from_db()
self.assertEqual(self.account.email, "alice@example.org")
def test_cn_named_account_sees_read_only_name_and_cannot_post(self):
Account.objects.filter(pk=self.account.pk).update(ldap_dn="cn=Alice Example," + SEARCH_DN)
self.read.side_effect = lambda account: copy.deepcopy(self.values)
response = self.client.get(self.profile_url)
self.assertFalse(response.context["name_editable"])
self.assertContains(response, "<strong>Alice Example</strong>")
self.assertNotContains(response, 'name="cn"')
self.assertNotContains(response, 'value="profile"')
self.assertContains(response, 'value="email"')
self.assertContains(response, 'value="password"')
self.assertEqual(self.client.post(self.profile_url, self.profile_data(cn="New Name")).status_code, 400)
self.assert_no_writes()
def test_photo_endpoint_removed(self):
with self.assertRaises(NoReverseMatch):
reverse("members:profile_photo")
@@ -640,6 +664,13 @@ class ProfileDirectoryTests(TestCase):
self.assertEqual(conn.modify.call_args.args, (ALICE_DN, {"cn": [(MODIFY_REPLACE, ["Changed"])]}))
self.assert_critical_assertion(conn.modify.call_args)
def test_profile_update_refuses_cn_named_entry(self):
conn, authorized = self.authorized()
self.account.ldap_dn = "cn=Alice Example," + SEARCH_DN
with patch("members.profiles._authorized", authorized), self.assertRaisesRegex(ValueError, "entry name"):
profiles.update_profile(self.account, self.cleaned(cn=["Changed"]), profiles.revision(self.account, self.values))
conn.modify.assert_not_called()
def test_profile_edit_without_timestamp_fails_closed(self):
values = profile_values(modifyTimestamp=[])
conn, authorized = self.authorized(values=values)