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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user