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
^^^^^
- LDAP user group removal.
- Display an error message when trying to remove the last user from a group.
[0.0.48] - 2024-04-08
---------------------

View file

@ -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

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):
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],
),
)

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

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):
"""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 &#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):
res = testclient.get("/profile/admin/settings")
res = testclient.post(