From 5aaccca4cfa96fc14e9dac44a244304fbf8702a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Mon, 8 Apr 2024 14:37:59 +0200 Subject: [PATCH] fix: display an error message when trying to remove the last user from a group --- CHANGES.rst | 1 + canaille/app/forms.py | 2 +- canaille/core/endpoints/forms.py | 19 +++ canaille/translations/messages.pot | 172 ++++++++++++++-------------- tests/core/test_profile_settings.py | 25 ++++ 5 files changed, 135 insertions(+), 84 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ee0cdc56..6e058476 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,7 @@ Fixed ^^^^^ - LDAP user group removal. +- Display an error message when trying to remove the last user from a group. [0.0.48] - 2024-04-08 --------------------- diff --git a/canaille/app/forms.py b/canaille/app/forms.py index 80ca0652..5b2e8805 100644 --- a/canaille/app/forms.py +++ b/canaille/app/forms.py @@ -256,7 +256,7 @@ class IDToModel: def __call__(self, data): model = getattr(models, self.model_name) - instance = data if isinstance(data, model) else model.get(id=data) + instance = data if isinstance(data, model) else model.get(data) if not instance: raise wtforms.ValidationError() return instance diff --git a/canaille/core/endpoints/forms.py b/canaille/core/endpoints/forms.py index d75af9a9..bddf7a7b 100644 --- a/canaille/core/endpoints/forms.py +++ b/canaille/core/endpoints/forms.py @@ -56,6 +56,24 @@ def existing_login(form, field): ) +def non_empty_groups(form, field): + """LDAP groups cannot be empty because groupOfNames.member is a MUST + attribute. + + https://www.rfc-editor.org/rfc/rfc2256.html#section-7.10 + """ + if not form.user: + return + + for group in form.user.groups: + if len(group.members) == 1 and group not in field.data: + raise wtforms.ValidationError( + _( + "The group '{group}' cannot be removed, because it must have at least one user left." + ).format(group=group.display_name) + ) + + class LoginForm(Form): login = wtforms.StringField( _("Login"), @@ -279,6 +297,7 @@ PROFILE_FORM_FIELDS = dict( choices=lambda: [(group, group.display_name) for group in models.Group.query()], render_kw={"placeholder": _("users, admins …")}, coerce=IDToModel("Group"), + validators=[non_empty_groups], ), ) diff --git a/canaille/translations/messages.pot b/canaille/translations/messages.pot index cccf3a82..8443ba72 100644 --- a/canaille/translations/messages.pot +++ b/canaille/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-04-07 19:49+0200\n" +"POT-Creation-Date: 2024-04-08 14:43+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -41,36 +41,36 @@ msgstr "" msgid "Not a valid datetime value." msgstr "" -#: canaille/backends/ldap/backend.py:94 +#: canaille/backends/ldap/backend.py:97 msgid "Could not connect to the LDAP server '{uri}'" msgstr "" -#: canaille/backends/ldap/backend.py:101 +#: canaille/backends/ldap/backend.py:104 msgid "LDAP authentication failed with user '{user}'" msgstr "" -#: canaille/backends/ldap/backend.py:170 +#: canaille/backends/ldap/backend.py:173 msgid "John Doe" msgstr "" -#: canaille/backends/ldap/backend.py:173 canaille/core/endpoints/forms.py:127 -#: canaille/core/endpoints/forms.py:375 +#: canaille/backends/ldap/backend.py:176 canaille/core/endpoints/forms.py:146 +#: canaille/core/endpoints/forms.py:395 msgid "jdoe" msgstr "" -#: canaille/backends/ldap/backend.py:176 +#: canaille/backends/ldap/backend.py:179 msgid "john@doe.com" msgstr "" -#: canaille/backends/ldap/backend.py:178 +#: canaille/backends/ldap/backend.py:181 msgid " or " msgstr "" -#: canaille/backends/ldap/models.py:98 +#: canaille/backends/ldap/backend.py:236 msgid "Your account has been locked." msgstr "" -#: canaille/backends/ldap/models.py:103 +#: canaille/backends/ldap/backend.py:241 msgid "You should change your password." msgstr "" @@ -128,8 +128,8 @@ msgstr "" msgid "You are already logged in, you cannot create an account." msgstr "" -#: canaille/core/endpoints/account.py:297 canaille/core/endpoints/forms.py:276 -#: canaille/core/endpoints/forms.py:393 canaille/core/templates/groups.html:5 +#: canaille/core/endpoints/account.py:297 canaille/core/endpoints/forms.py:295 +#: canaille/core/endpoints/forms.py:413 canaille/core/templates/groups.html:5 #: canaille/core/templates/groups.html:23 #: canaille/core/templates/partial/users.html:18 #: canaille/templates/base.html:57 @@ -175,12 +175,12 @@ msgid "User account creation succeed." msgstr "" #: canaille/core/endpoints/account.py:609 -#: canaille/core/endpoints/account.py:771 +#: canaille/core/endpoints/account.py:770 msgid "Profile edition failed." msgstr "" #: canaille/core/endpoints/account.py:613 -#: canaille/core/endpoints/account.py:786 +#: canaille/core/endpoints/account.py:785 msgid "Profile updated successfully." msgstr "" @@ -212,7 +212,7 @@ msgid "" "It should be received within a few minutes." msgstr "" -#: canaille/core/endpoints/account.py:689 canaille/core/endpoints/auth.py:137 +#: canaille/core/endpoints/account.py:689 canaille/core/endpoints/auth.py:136 msgid "Could not send the password initialization email" msgstr "" @@ -234,12 +234,12 @@ msgstr "" msgid "The account has been unlocked" msgstr "" -#: canaille/core/endpoints/account.py:806 +#: canaille/core/endpoints/account.py:805 #, python-format msgid "The user %(user)s has been sucessfuly deleted" msgstr "" -#: canaille/core/endpoints/account.py:824 canaille/core/endpoints/auth.py:93 +#: canaille/core/endpoints/account.py:823 canaille/core/endpoints/auth.py:92 #, python-format msgid "Connection successful. Welcome %(user)s" msgstr "" @@ -249,10 +249,10 @@ msgstr "" msgid "Email" msgstr "" -#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:63 -#: canaille/core/endpoints/forms.py:83 canaille/core/endpoints/forms.py:172 -#: canaille/core/endpoints/forms.py:361 canaille/core/endpoints/forms.py:387 -#: canaille/core/endpoints/forms.py:408 canaille/core/endpoints/forms.py:424 +#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:82 +#: canaille/core/endpoints/forms.py:102 canaille/core/endpoints/forms.py:191 +#: canaille/core/endpoints/forms.py:381 canaille/core/endpoints/forms.py:407 +#: canaille/core/endpoints/forms.py:428 canaille/core/endpoints/forms.py:444 msgid "jane@doe.com" msgstr "" @@ -272,96 +272,102 @@ msgstr "" msgid "Email confirmation on {website_name}" msgstr "" -#: canaille/core/endpoints/auth.py:51 canaille/core/endpoints/auth.py:77 -#: canaille/core/endpoints/auth.py:85 +#: canaille/core/endpoints/auth.py:50 canaille/core/endpoints/auth.py:76 +#: canaille/core/endpoints/auth.py:84 msgid "Login failed, please check your information" msgstr "" -#: canaille/core/endpoints/auth.py:105 +#: canaille/core/endpoints/auth.py:104 #, python-format msgid "You have been disconnected. See you next time %(user)s" msgstr "" -#: canaille/core/endpoints/auth.py:130 +#: canaille/core/endpoints/auth.py:129 msgid "" "A password initialization link has been sent at your email address. You " "should receive it within a few minutes." msgstr "" -#: canaille/core/endpoints/auth.py:153 +#: canaille/core/endpoints/auth.py:152 msgid "Could not send the password reset link." msgstr "" -#: canaille/core/endpoints/auth.py:157 +#: canaille/core/endpoints/auth.py:156 msgid "" "A password reset link has been sent at your email address. You should " "receive it within a few minutes." msgstr "" -#: canaille/core/endpoints/auth.py:169 +#: canaille/core/endpoints/auth.py:168 #, python-format msgid "" "The user '%(user)s' does not have permissions to update their password. " "We cannot send a password reset email." msgstr "" -#: canaille/core/endpoints/auth.py:185 +#: canaille/core/endpoints/auth.py:184 msgid "We encountered an issue while we sent the password recovery email." msgstr "" -#: canaille/core/endpoints/auth.py:208 +#: canaille/core/endpoints/auth.py:207 msgid "The password reset link that brought you here was invalid." msgstr "" -#: canaille/core/endpoints/auth.py:217 +#: canaille/core/endpoints/auth.py:216 msgid "Your password has been updated successfully" msgstr "" -#: canaille/core/endpoints/forms.py:29 +#: canaille/core/endpoints/forms.py:30 msgid "The user name '{user_name}' already exists" msgstr "" -#: canaille/core/endpoints/forms.py:38 +#: canaille/core/endpoints/forms.py:39 msgid "The email '{email}' is already used" msgstr "" -#: canaille/core/endpoints/forms.py:45 +#: canaille/core/endpoints/forms.py:46 msgid "The group '{group}' already exists" msgstr "" -#: canaille/core/endpoints/forms.py:54 +#: canaille/core/endpoints/forms.py:55 msgid "The login '{login}' does not exist" msgstr "" -#: canaille/core/endpoints/forms.py:60 canaille/core/endpoints/forms.py:80 +#: canaille/core/endpoints/forms.py:71 +msgid "" +"The group '{group}' cannot be removed, because it must have at least one " +"user left." +msgstr "" + +#: canaille/core/endpoints/forms.py:79 canaille/core/endpoints/forms.py:99 #: canaille/core/templates/partial/users.html:9 msgid "Login" msgstr "" -#: canaille/core/endpoints/forms.py:73 canaille/core/endpoints/forms.py:92 -#: canaille/core/endpoints/forms.py:226 +#: canaille/core/endpoints/forms.py:92 canaille/core/endpoints/forms.py:111 +#: canaille/core/endpoints/forms.py:245 #: canaille/core/templates/profile_settings.html:63 msgid "Password" msgstr "" -#: canaille/core/endpoints/forms.py:99 canaille/core/endpoints/forms.py:236 +#: canaille/core/endpoints/forms.py:118 canaille/core/endpoints/forms.py:255 msgid "Password confirmation" msgstr "" -#: canaille/core/endpoints/forms.py:102 canaille/core/endpoints/forms.py:239 +#: canaille/core/endpoints/forms.py:121 canaille/core/endpoints/forms.py:258 msgid "Password and confirmation do not match." msgstr "" -#: canaille/core/endpoints/forms.py:121 +#: canaille/core/endpoints/forms.py:140 msgid "Automatic" msgstr "" -#: canaille/core/endpoints/forms.py:126 +#: canaille/core/endpoints/forms.py:145 msgid "Username" msgstr "" -#: canaille/core/endpoints/forms.py:130 canaille/core/endpoints/forms.py:324 -#: canaille/core/endpoints/forms.py:338 +#: canaille/core/endpoints/forms.py:149 canaille/core/endpoints/forms.py:344 +#: canaille/core/endpoints/forms.py:358 #: canaille/core/templates/partial/groups.html:6 #: canaille/core/templates/partial/users.html:12 #: canaille/oidc/endpoints/forms.py:26 @@ -369,165 +375,165 @@ msgstr "" msgid "Name" msgstr "" -#: canaille/core/endpoints/forms.py:132 +#: canaille/core/endpoints/forms.py:151 msgid "Title" msgstr "" -#: canaille/core/endpoints/forms.py:132 +#: canaille/core/endpoints/forms.py:151 msgid "Vice president" msgstr "" -#: canaille/core/endpoints/forms.py:135 +#: canaille/core/endpoints/forms.py:154 msgid "Given name" msgstr "" -#: canaille/core/endpoints/forms.py:137 +#: canaille/core/endpoints/forms.py:156 msgid "John" msgstr "" -#: canaille/core/endpoints/forms.py:143 +#: canaille/core/endpoints/forms.py:162 msgid "Family Name" msgstr "" -#: canaille/core/endpoints/forms.py:146 +#: canaille/core/endpoints/forms.py:165 msgid "Doe" msgstr "" -#: canaille/core/endpoints/forms.py:152 +#: canaille/core/endpoints/forms.py:171 msgid "Display Name" msgstr "" -#: canaille/core/endpoints/forms.py:155 +#: canaille/core/endpoints/forms.py:174 msgid "Johnny" msgstr "" -#: canaille/core/endpoints/forms.py:162 canaille/core/endpoints/forms.py:414 +#: canaille/core/endpoints/forms.py:181 canaille/core/endpoints/forms.py:434 #: canaille/core/templates/profile_edit.html:176 msgid "Email addresses" msgstr "" -#: canaille/core/endpoints/forms.py:168 canaille/core/endpoints/forms.py:404 +#: canaille/core/endpoints/forms.py:187 canaille/core/endpoints/forms.py:424 msgid "" "This email will be used as a recovery address to reset the password if " "needed" msgstr "" -#: canaille/core/endpoints/forms.py:182 +#: canaille/core/endpoints/forms.py:201 msgid "Phone numbers" msgstr "" -#: canaille/core/endpoints/forms.py:183 +#: canaille/core/endpoints/forms.py:202 msgid "555-000-555" msgstr "" -#: canaille/core/endpoints/forms.py:190 +#: canaille/core/endpoints/forms.py:209 msgid "Address" msgstr "" -#: canaille/core/endpoints/forms.py:192 +#: canaille/core/endpoints/forms.py:211 msgid "132, Foobar Street, Gotham City 12401, XX" msgstr "" -#: canaille/core/endpoints/forms.py:196 +#: canaille/core/endpoints/forms.py:215 msgid "Street" msgstr "" -#: canaille/core/endpoints/forms.py:198 +#: canaille/core/endpoints/forms.py:217 msgid "132, Foobar Street" msgstr "" -#: canaille/core/endpoints/forms.py:202 +#: canaille/core/endpoints/forms.py:221 msgid "Postal Code" msgstr "" -#: canaille/core/endpoints/forms.py:208 +#: canaille/core/endpoints/forms.py:227 msgid "Locality" msgstr "" -#: canaille/core/endpoints/forms.py:210 +#: canaille/core/endpoints/forms.py:229 msgid "Gotham City" msgstr "" -#: canaille/core/endpoints/forms.py:214 +#: canaille/core/endpoints/forms.py:233 msgid "Region" msgstr "" -#: canaille/core/endpoints/forms.py:216 +#: canaille/core/endpoints/forms.py:235 msgid "North Pole" msgstr "" -#: canaille/core/endpoints/forms.py:220 +#: canaille/core/endpoints/forms.py:239 msgid "Photo" msgstr "" -#: canaille/core/endpoints/forms.py:224 +#: canaille/core/endpoints/forms.py:243 #: canaille/core/templates/profile_add.html:56 #: canaille/core/templates/profile_edit.html:64 msgid "Delete the photo" msgstr "" -#: canaille/core/endpoints/forms.py:247 +#: canaille/core/endpoints/forms.py:266 msgid "User number" msgstr "" -#: canaille/core/endpoints/forms.py:249 canaille/core/endpoints/forms.py:255 +#: canaille/core/endpoints/forms.py:268 canaille/core/endpoints/forms.py:274 msgid "1234" msgstr "" -#: canaille/core/endpoints/forms.py:253 +#: canaille/core/endpoints/forms.py:272 msgid "Department" msgstr "" -#: canaille/core/endpoints/forms.py:259 +#: canaille/core/endpoints/forms.py:278 msgid "Organization" msgstr "" -#: canaille/core/endpoints/forms.py:261 +#: canaille/core/endpoints/forms.py:280 msgid "Cogip LTD." msgstr "" -#: canaille/core/endpoints/forms.py:265 +#: canaille/core/endpoints/forms.py:284 msgid "Website" msgstr "" -#: canaille/core/endpoints/forms.py:267 +#: canaille/core/endpoints/forms.py:286 msgid "https://mywebsite.tld" msgstr "" -#: canaille/core/endpoints/forms.py:272 +#: canaille/core/endpoints/forms.py:291 msgid "Preferred language" msgstr "" -#: canaille/core/endpoints/forms.py:279 +#: canaille/core/endpoints/forms.py:298 msgid "users, admins …" msgstr "" -#: canaille/core/endpoints/forms.py:303 +#: canaille/core/endpoints/forms.py:323 msgid "Account expiration" msgstr "" -#: canaille/core/endpoints/forms.py:327 +#: canaille/core/endpoints/forms.py:347 msgid "group" msgstr "" -#: canaille/core/endpoints/forms.py:331 canaille/core/endpoints/forms.py:348 +#: canaille/core/endpoints/forms.py:351 canaille/core/endpoints/forms.py:368 #: canaille/core/templates/partial/groups.html:7 msgid "Description" msgstr "" -#: canaille/core/endpoints/forms.py:355 canaille/core/endpoints/forms.py:380 +#: canaille/core/endpoints/forms.py:375 canaille/core/endpoints/forms.py:400 msgid "Email address" msgstr "" -#: canaille/core/endpoints/forms.py:374 +#: canaille/core/endpoints/forms.py:394 msgid "User name" msgstr "" -#: canaille/core/endpoints/forms.py:378 +#: canaille/core/endpoints/forms.py:398 msgid "Username editable by the invitee" msgstr "" -#: canaille/core/endpoints/forms.py:417 +#: canaille/core/endpoints/forms.py:437 msgid "New email address" msgstr "" diff --git a/tests/core/test_profile_settings.py b/tests/core/test_profile_settings.py index 83c23a53..43024289 100644 --- a/tests/core/test_profile_settings.py +++ b/tests/core/test_profile_settings.py @@ -38,6 +38,7 @@ def test_edition(testclient, logged_user, admin, foo_group, bar_group, backend): def test_group_removal(testclient, logged_admin, user, foo_group, backend): + """Tests that one can remove a group from a user.""" foo_group.members = [user, logged_admin] foo_group.save() user.reload() @@ -52,9 +53,33 @@ def test_group_removal(testclient, logged_admin, user, foo_group, backend): assert foo_group not in user.groups foo_group.reload() + logged_admin.reload() assert foo_group.members == [logged_admin] +def test_empty_group_removal(testclient, logged_admin, user, foo_group, backend): + """Tests that one cannot remove a group from a user, when was the last + person in the group. + + This is because LDAP groups cannot be empty because + groupOfNames.member is a MUST attribute. + https://www.rfc-editor.org/rfc/rfc2256.html#section-7.10 + """ + assert foo_group in user.groups + + res = testclient.get("/profile/user/settings", status=200) + res.form["groups"] = [] + res = res.form.submit(name="action", value="edit-settings") + assert res.flashes == [("error", "Profile edition failed.")] + + res.mustcontain( + "The group 'foo' cannot be removed, because it must have at least one user left." + ) + + user.reload() + assert foo_group in user.groups + + def test_profile_settings_edition_dynamic_validation(testclient, logged_admin): res = testclient.get("/profile/admin/settings") res = testclient.post(