mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 22:05:50 +01:00
fix(users): apply user updates only after the payload validates (#1360)
`PUT /api/users/me` and `PUT /api/admin/users/<id>` write the new password hash, and then the profile fields, before the rest of the payload is checked. When the request is rejected further down as an invalid role, an admin-only setting, an invalid settings value, the route answers 400 with those writes already committed, so the caller sees an error while the password has in fact changed. Both routes now validate the whole payload before touching the database, and the password hash is folded into the same `update_user` call as the other fields so the field write is a single transaction. Error messages, status codes and the order they are reported in are unchanged. ## Verification - New tests in `tests/core/test_self_user_routes.py` and `tests/core/test_admin_users_api.py` assert that a rejected update leaves the password, profile fields and role as they were, plus a positive case that a valid payload still applies all three. They fail on current main and pass here. - Full suite (3150), ruff, ruff format, basedpyright, vulture green.
This commit is contained in:
@@ -980,6 +980,40 @@ class TestAdminUserPasswordUpdate:
|
||||
assert resp.status_code == 400
|
||||
assert "Cannot set password for PROXY users" in resp.json["error"]
|
||||
|
||||
def test_update_password_not_applied_when_role_is_rejected(self, admin_client, user_db):
|
||||
"""A rejected role leaves the stored password untouched."""
|
||||
user = user_db.create_user(username="alice", role="user", password_hash="old_hash")
|
||||
|
||||
resp = admin_client.put(
|
||||
f"/api/admin/users/{user['id']}",
|
||||
json={"password": "newpass99", "role": "superuser"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json["error"] == "Role must be 'admin' or 'user'"
|
||||
|
||||
updated = user_db.get_user(user_id=user["id"])
|
||||
assert updated["password_hash"] == "old_hash"
|
||||
assert updated["role"] == "user"
|
||||
|
||||
def test_update_password_not_applied_when_settings_are_rejected(self, admin_client, user_db):
|
||||
"""A rejected settings payload leaves the password and fields untouched."""
|
||||
user = user_db.create_user(username="alice", role="user", password_hash="old_hash")
|
||||
|
||||
resp = admin_client.put(
|
||||
f"/api/admin/users/{user['id']}",
|
||||
json={
|
||||
"password": "newpass99",
|
||||
"role": "admin",
|
||||
"settings": {"BOOK_LANGUAGE": ["klingon"]},
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json["error"] == "Invalid settings payload"
|
||||
|
||||
updated = user_db.get_user(user_id=user["id"])
|
||||
assert updated["password_hash"] == "old_hash"
|
||||
assert updated["role"] == "user"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/admin/users/sync-cwa
|
||||
|
||||
Reference in New Issue
Block a user