fix: display an error message when trying to remove the last user from a group

This commit is contained in:
Éloi Rivard 2024-04-08 14:37:59 +02:00
parent fe2665ae32
commit 5aaccca4cf
No known key found for this signature in database
GPG key ID: 7EDA204EA57DD184
5 changed files with 135 additions and 84 deletions

View file

@ -2,6 +2,7 @@
Fixed Fixed
^^^^^ ^^^^^
- LDAP user group removal. - LDAP user group removal.
- Display an error message when trying to remove the last user from a group.
[0.0.48] - 2024-04-08 [0.0.48] - 2024-04-08
--------------------- ---------------------

View file

@ -256,7 +256,7 @@ class IDToModel:
def __call__(self, data): def __call__(self, data):
model = getattr(models, self.model_name) 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: if not instance:
raise wtforms.ValidationError() raise wtforms.ValidationError()
return instance return instance

View file

@ -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): class LoginForm(Form):
login = wtforms.StringField( login = wtforms.StringField(
_("Login"), _("Login"),
@ -279,6 +297,7 @@ PROFILE_FORM_FIELDS = dict(
choices=lambda: [(group, group.display_name) for group in models.Group.query()], choices=lambda: [(group, group.display_name) for group in models.Group.query()],
render_kw={"placeholder": _("users, admins …")}, render_kw={"placeholder": _("users, admins …")},
coerce=IDToModel("Group"), coerce=IDToModel("Group"),
validators=[non_empty_groups],
), ),
) )

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -41,36 +41,36 @@ msgstr ""
msgid "Not a valid datetime value." msgid "Not a valid datetime value."
msgstr "" msgstr ""
#: canaille/backends/ldap/backend.py:94 #: canaille/backends/ldap/backend.py:97
msgid "Could not connect to the LDAP server '{uri}'" msgid "Could not connect to the LDAP server '{uri}'"
msgstr "" msgstr ""
#: canaille/backends/ldap/backend.py:101 #: canaille/backends/ldap/backend.py:104
msgid "LDAP authentication failed with user '{user}'" msgid "LDAP authentication failed with user '{user}'"
msgstr "" msgstr ""
#: canaille/backends/ldap/backend.py:170 #: canaille/backends/ldap/backend.py:173
msgid "John Doe" msgid "John Doe"
msgstr "" msgstr ""
#: canaille/backends/ldap/backend.py:173 canaille/core/endpoints/forms.py:127 #: canaille/backends/ldap/backend.py:176 canaille/core/endpoints/forms.py:146
#: canaille/core/endpoints/forms.py:375 #: canaille/core/endpoints/forms.py:395
msgid "jdoe" msgid "jdoe"
msgstr "" msgstr ""
#: canaille/backends/ldap/backend.py:176 #: canaille/backends/ldap/backend.py:179
msgid "john@doe.com" msgid "john@doe.com"
msgstr "" msgstr ""
#: canaille/backends/ldap/backend.py:178 #: canaille/backends/ldap/backend.py:181
msgid " or " msgid " or "
msgstr "" msgstr ""
#: canaille/backends/ldap/models.py:98 #: canaille/backends/ldap/backend.py:236
msgid "Your account has been locked." msgid "Your account has been locked."
msgstr "" msgstr ""
#: canaille/backends/ldap/models.py:103 #: canaille/backends/ldap/backend.py:241
msgid "You should change your password." msgid "You should change your password."
msgstr "" msgstr ""
@ -128,8 +128,8 @@ msgstr ""
msgid "You are already logged in, you cannot create an account." msgid "You are already logged in, you cannot create an account."
msgstr "" msgstr ""
#: canaille/core/endpoints/account.py:297 canaille/core/endpoints/forms.py:276 #: canaille/core/endpoints/account.py:297 canaille/core/endpoints/forms.py:295
#: canaille/core/endpoints/forms.py:393 canaille/core/templates/groups.html:5 #: canaille/core/endpoints/forms.py:413 canaille/core/templates/groups.html:5
#: canaille/core/templates/groups.html:23 #: canaille/core/templates/groups.html:23
#: canaille/core/templates/partial/users.html:18 #: canaille/core/templates/partial/users.html:18
#: canaille/templates/base.html:57 #: canaille/templates/base.html:57
@ -175,12 +175,12 @@ msgid "User account creation succeed."
msgstr "" msgstr ""
#: canaille/core/endpoints/account.py:609 #: canaille/core/endpoints/account.py:609
#: canaille/core/endpoints/account.py:771 #: canaille/core/endpoints/account.py:770
msgid "Profile edition failed." msgid "Profile edition failed."
msgstr "" msgstr ""
#: canaille/core/endpoints/account.py:613 #: canaille/core/endpoints/account.py:613
#: canaille/core/endpoints/account.py:786 #: canaille/core/endpoints/account.py:785
msgid "Profile updated successfully." msgid "Profile updated successfully."
msgstr "" msgstr ""
@ -212,7 +212,7 @@ msgid ""
"It should be received within a few minutes." "It should be received within a few minutes."
msgstr "" 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" msgid "Could not send the password initialization email"
msgstr "" msgstr ""
@ -234,12 +234,12 @@ msgstr ""
msgid "The account has been unlocked" msgid "The account has been unlocked"
msgstr "" msgstr ""
#: canaille/core/endpoints/account.py:806 #: canaille/core/endpoints/account.py:805
#, python-format #, python-format
msgid "The user %(user)s has been sucessfuly deleted" msgid "The user %(user)s has been sucessfuly deleted"
msgstr "" 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 #, python-format
msgid "Connection successful. Welcome %(user)s" msgid "Connection successful. Welcome %(user)s"
msgstr "" msgstr ""
@ -249,10 +249,10 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:63 #: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:82
#: canaille/core/endpoints/forms.py:83 canaille/core/endpoints/forms.py:172 #: canaille/core/endpoints/forms.py:102 canaille/core/endpoints/forms.py:191
#: canaille/core/endpoints/forms.py:361 canaille/core/endpoints/forms.py:387 #: canaille/core/endpoints/forms.py:381 canaille/core/endpoints/forms.py:407
#: canaille/core/endpoints/forms.py:408 canaille/core/endpoints/forms.py:424 #: canaille/core/endpoints/forms.py:428 canaille/core/endpoints/forms.py:444
msgid "jane@doe.com" msgid "jane@doe.com"
msgstr "" msgstr ""
@ -272,96 +272,102 @@ msgstr ""
msgid "Email confirmation on {website_name}" msgid "Email confirmation on {website_name}"
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:51 canaille/core/endpoints/auth.py:77 #: canaille/core/endpoints/auth.py:50 canaille/core/endpoints/auth.py:76
#: canaille/core/endpoints/auth.py:85 #: canaille/core/endpoints/auth.py:84
msgid "Login failed, please check your information" msgid "Login failed, please check your information"
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:105 #: canaille/core/endpoints/auth.py:104
#, python-format #, python-format
msgid "You have been disconnected. See you next time %(user)s" msgid "You have been disconnected. See you next time %(user)s"
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:130 #: canaille/core/endpoints/auth.py:129
msgid "" msgid ""
"A password initialization link has been sent at your email address. You " "A password initialization link has been sent at your email address. You "
"should receive it within a few minutes." "should receive it within a few minutes."
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:153 #: canaille/core/endpoints/auth.py:152
msgid "Could not send the password reset link." msgid "Could not send the password reset link."
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:157 #: canaille/core/endpoints/auth.py:156
msgid "" msgid ""
"A password reset link has been sent at your email address. You should " "A password reset link has been sent at your email address. You should "
"receive it within a few minutes." "receive it within a few minutes."
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:169 #: canaille/core/endpoints/auth.py:168
#, python-format #, python-format
msgid "" msgid ""
"The user '%(user)s' does not have permissions to update their password. " "The user '%(user)s' does not have permissions to update their password. "
"We cannot send a password reset email." "We cannot send a password reset email."
msgstr "" 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." msgid "We encountered an issue while we sent the password recovery email."
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:208 #: canaille/core/endpoints/auth.py:207
msgid "The password reset link that brought you here was invalid." msgid "The password reset link that brought you here was invalid."
msgstr "" msgstr ""
#: canaille/core/endpoints/auth.py:217 #: canaille/core/endpoints/auth.py:216
msgid "Your password has been updated successfully" msgid "Your password has been updated successfully"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:29 #: canaille/core/endpoints/forms.py:30
msgid "The user name '{user_name}' already exists" msgid "The user name '{user_name}' already exists"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:38 #: canaille/core/endpoints/forms.py:39
msgid "The email '{email}' is already used" msgid "The email '{email}' is already used"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:45 #: canaille/core/endpoints/forms.py:46
msgid "The group '{group}' already exists" msgid "The group '{group}' already exists"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:54 #: canaille/core/endpoints/forms.py:55
msgid "The login '{login}' does not exist" msgid "The login '{login}' does not exist"
msgstr "" 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 #: canaille/core/templates/partial/users.html:9
msgid "Login" msgid "Login"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:73 canaille/core/endpoints/forms.py:92 #: canaille/core/endpoints/forms.py:92 canaille/core/endpoints/forms.py:111
#: canaille/core/endpoints/forms.py:226 #: canaille/core/endpoints/forms.py:245
#: canaille/core/templates/profile_settings.html:63 #: canaille/core/templates/profile_settings.html:63
msgid "Password" msgid "Password"
msgstr "" 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" msgid "Password confirmation"
msgstr "" 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." msgid "Password and confirmation do not match."
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:121 #: canaille/core/endpoints/forms.py:140
msgid "Automatic" msgid "Automatic"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:126 #: canaille/core/endpoints/forms.py:145
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:130 canaille/core/endpoints/forms.py:324 #: canaille/core/endpoints/forms.py:149 canaille/core/endpoints/forms.py:344
#: canaille/core/endpoints/forms.py:338 #: canaille/core/endpoints/forms.py:358
#: canaille/core/templates/partial/groups.html:6 #: canaille/core/templates/partial/groups.html:6
#: canaille/core/templates/partial/users.html:12 #: canaille/core/templates/partial/users.html:12
#: canaille/oidc/endpoints/forms.py:26 #: canaille/oidc/endpoints/forms.py:26
@ -369,165 +375,165 @@ msgstr ""
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:132 #: canaille/core/endpoints/forms.py:151
msgid "Title" msgid "Title"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:132 #: canaille/core/endpoints/forms.py:151
msgid "Vice president" msgid "Vice president"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:135 #: canaille/core/endpoints/forms.py:154
msgid "Given name" msgid "Given name"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:137 #: canaille/core/endpoints/forms.py:156
msgid "John" msgid "John"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:143 #: canaille/core/endpoints/forms.py:162
msgid "Family Name" msgid "Family Name"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:146 #: canaille/core/endpoints/forms.py:165
msgid "Doe" msgid "Doe"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:152 #: canaille/core/endpoints/forms.py:171
msgid "Display Name" msgid "Display Name"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:155 #: canaille/core/endpoints/forms.py:174
msgid "Johnny" msgid "Johnny"
msgstr "" 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 #: canaille/core/templates/profile_edit.html:176
msgid "Email addresses" msgid "Email addresses"
msgstr "" 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 "" msgid ""
"This email will be used as a recovery address to reset the password if " "This email will be used as a recovery address to reset the password if "
"needed" "needed"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:182 #: canaille/core/endpoints/forms.py:201
msgid "Phone numbers" msgid "Phone numbers"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:183 #: canaille/core/endpoints/forms.py:202
msgid "555-000-555" msgid "555-000-555"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:190 #: canaille/core/endpoints/forms.py:209
msgid "Address" msgid "Address"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:192 #: canaille/core/endpoints/forms.py:211
msgid "132, Foobar Street, Gotham City 12401, XX" msgid "132, Foobar Street, Gotham City 12401, XX"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:196 #: canaille/core/endpoints/forms.py:215
msgid "Street" msgid "Street"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:198 #: canaille/core/endpoints/forms.py:217
msgid "132, Foobar Street" msgid "132, Foobar Street"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:202 #: canaille/core/endpoints/forms.py:221
msgid "Postal Code" msgid "Postal Code"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:208 #: canaille/core/endpoints/forms.py:227
msgid "Locality" msgid "Locality"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:210 #: canaille/core/endpoints/forms.py:229
msgid "Gotham City" msgid "Gotham City"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:214 #: canaille/core/endpoints/forms.py:233
msgid "Region" msgid "Region"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:216 #: canaille/core/endpoints/forms.py:235
msgid "North Pole" msgid "North Pole"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:220 #: canaille/core/endpoints/forms.py:239
msgid "Photo" msgid "Photo"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:224 #: canaille/core/endpoints/forms.py:243
#: canaille/core/templates/profile_add.html:56 #: canaille/core/templates/profile_add.html:56
#: canaille/core/templates/profile_edit.html:64 #: canaille/core/templates/profile_edit.html:64
msgid "Delete the photo" msgid "Delete the photo"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:247 #: canaille/core/endpoints/forms.py:266
msgid "User number" msgid "User number"
msgstr "" 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" msgid "1234"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:253 #: canaille/core/endpoints/forms.py:272
msgid "Department" msgid "Department"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:259 #: canaille/core/endpoints/forms.py:278
msgid "Organization" msgid "Organization"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:261 #: canaille/core/endpoints/forms.py:280
msgid "Cogip LTD." msgid "Cogip LTD."
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:265 #: canaille/core/endpoints/forms.py:284
msgid "Website" msgid "Website"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:267 #: canaille/core/endpoints/forms.py:286
msgid "https://mywebsite.tld" msgid "https://mywebsite.tld"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:272 #: canaille/core/endpoints/forms.py:291
msgid "Preferred language" msgid "Preferred language"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:279 #: canaille/core/endpoints/forms.py:298
msgid "users, admins …" msgid "users, admins …"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:303 #: canaille/core/endpoints/forms.py:323
msgid "Account expiration" msgid "Account expiration"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:327 #: canaille/core/endpoints/forms.py:347
msgid "group" msgid "group"
msgstr "" 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 #: canaille/core/templates/partial/groups.html:7
msgid "Description" msgid "Description"
msgstr "" 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" msgid "Email address"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:374 #: canaille/core/endpoints/forms.py:394
msgid "User name" msgid "User name"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:378 #: canaille/core/endpoints/forms.py:398
msgid "Username editable by the invitee" msgid "Username editable by the invitee"
msgstr "" msgstr ""
#: canaille/core/endpoints/forms.py:417 #: canaille/core/endpoints/forms.py:437
msgid "New email address" msgid "New email address"
msgstr "" msgstr ""

View file

@ -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): 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.members = [user, logged_admin]
foo_group.save() foo_group.save()
user.reload() user.reload()
@ -52,9 +53,33 @@ def test_group_removal(testclient, logged_admin, user, foo_group, backend):
assert foo_group not in user.groups assert foo_group not in user.groups
foo_group.reload() foo_group.reload()
logged_admin.reload()
assert foo_group.members == [logged_admin] 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 &#39;foo&#39; 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): def test_profile_settings_edition_dynamic_validation(testclient, logged_admin):
res = testclient.get("/profile/admin/settings") res = testclient.get("/profile/admin/settings")
res = testclient.post( res = testclient.post(