diff --git a/documentation/changelog.rst b/documentation/changelog.rst index f5f715450..29d62ce88 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -15,6 +15,7 @@ New features Bugfixes ----------- * Fix missing conversion of data source names and ids to DataSource objects [see `PR #178 `_] +* Fix users resetting their own password [see `PR #195 `_] Infrastructure / Support ---------------------- diff --git a/flexmeasures/api/v2_0/implementations/users.py b/flexmeasures/api/v2_0/implementations/users.py index ac7904629..73dced946 100644 --- a/flexmeasures/api/v2_0/implementations/users.py +++ b/flexmeasures/api/v2_0/implementations/users.py @@ -123,13 +123,16 @@ def patch(db_user: UserModel, user_data: dict): return user_schema.dump(db_user), 200 -@load_user(admins_only=True) +@load_user() @as_json def reset_password(user): """ Reset the user's current password, cookies and auth tokens. Send a password reset link to the user. """ + # Don't allow non-admins to reset passwords of other users + if current_user.id != user.id and not current_user.has_role("admin"): + return unauthorized_handler(None, []) set_random_password(user) remove_cookie_and_token_access(user) send_reset_password_instructions(user) diff --git a/flexmeasures/api/v2_0/routes.py b/flexmeasures/api/v2_0/routes.py index 2a0dbb033..cedc760e8 100644 --- a/flexmeasures/api/v2_0/routes.py +++ b/flexmeasures/api/v2_0/routes.py @@ -487,7 +487,7 @@ def reset_user_password(id: int): It sets the current password to something random, invalidates cookies and auth tokens, and also sends an email for resetting the password to the user. - Only admins can use this endpoint. + Users can reset their own passwords. Only admins can use this endpoint to reset passwords of other users. :reqheader Authorization: The authentication token :reqheader Content-Type: application/json diff --git a/flexmeasures/api/v2_0/tests/test_api_v2_0_users_fresh_db.py b/flexmeasures/api/v2_0/tests/test_api_v2_0_users_fresh_db.py index cfb2da44d..15741f087 100644 --- a/flexmeasures/api/v2_0/tests/test_api_v2_0_users_fresh_db.py +++ b/flexmeasures/api/v2_0/tests/test_api_v2_0_users_fresh_db.py @@ -11,14 +11,13 @@ (""), ("test_supplier@seita.nl"), ("test_prosumer@seita.nl"), - ("test_prosumer@seita.nl"), - ("test_prosumer@seita.nl"), + ("inactive@seita.nl"), ), ) -def test_user_reset_password(app, client, sender): +def test_user_reset_password(app, client, setup_inactive_user, sender): """ Reset the password of supplier. - Only the prosumer is allowed to do that (as admin). + Only the prosumer (as admin) and the supplier themselves are allowed to do that. """ with UserContext("test_supplier@seita.nl") as supplier: supplier_id = supplier.id @@ -34,14 +33,10 @@ def test_user_reset_password(app, client, sender): ) print("Server responded with:\n%s" % pwd_reset_response.json) - if sender == "": + if sender in ("", "inactive@seita.nl"): assert pwd_reset_response.status_code == 401 return - if sender == "test_supplier@seita.nl": - assert pwd_reset_response.status_code == 403 - return - assert pwd_reset_response.status_code == 200 supplier = find_user_by_email("test_supplier@seita.nl") diff --git a/flexmeasures/data/models/user.py b/flexmeasures/data/models/user.py index 3774132e3..bb4ddc210 100644 --- a/flexmeasures/data/models/user.py +++ b/flexmeasures/data/models/user.py @@ -92,7 +92,7 @@ class User(db.Model, UserMixin): ) def __repr__(self): - return "" % (self.username, self.id) @property def is_authenticated(self): diff --git a/flexmeasures/ui/crud/users.py b/flexmeasures/ui/crud/users.py index e7fd70aeb..f1bf4f1eb 100644 --- a/flexmeasures/ui/crud/users.py +++ b/flexmeasures/ui/crud/users.py @@ -6,7 +6,7 @@ from flask_wtf import FlaskForm from wtforms import StringField, FloatField, DateTimeField, BooleanField from wtforms.validators import DataRequired -from flask_security import roles_required +from flask_security import roles_required, login_required from flexmeasures.data.config import db from flexmeasures.data.models.user import User, Role, Account @@ -126,7 +126,7 @@ def toggle_active(self, id: str): % (patched_user.username, patched_user.active), ) - @roles_required("admin") + @login_required def reset_password_for(self, id: str): """/users/reset_password_for/ Set the password to something random (in case of worries the password might be compromised) @@ -138,5 +138,6 @@ def reset_password_for(self, id: str): return render_user( user, msg="The user's password has been changed to a random password" - " and password reset instructions have been sent to the user.", + " and password reset instructions have been sent to the user." + " Cookies and the API access token have also been invalidated.", ) diff --git a/flexmeasures/ui/tests/test_user_crud.py b/flexmeasures/ui/tests/test_user_crud.py index fa7e8a8be..59ed29e15 100644 --- a/flexmeasures/ui/tests/test_user_crud.py +++ b/flexmeasures/ui/tests/test_user_crud.py @@ -10,9 +10,7 @@ """ -@pytest.mark.parametrize( - "view", ["index", "get", "toggle_active", "reset_password_for"] -) +@pytest.mark.parametrize("view", ["index", "get", "toggle_active"]) def test_user_crud_as_non_admin(client, as_prosumer, view): user_index = client.get(url_for("UserCrudUI:index"), follow_redirects=True) assert user_index.status_code == 403