1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

fix(admin): setting a user's password deletes the user's API tokens

An administrator resets a password to close an account someone else
took over, but the API tokens stayed valid: a stolen token kept working
with the new password, for as long as it was made to last. Setting a
password on the user's Password page now deletes that user's tokens,
and the page says so. A user who changes their own password keeps their
tokens (scripts keep working); they can delete them on their profile.
This commit is contained in:
catborise 2026-11-06 19:54:20 +03:00
parent 4f7bca9754
commit c5d0672c68
8 changed files with 39 additions and 20 deletions

View file

@ -16,7 +16,7 @@
<div class="row g-3">
<div class="col-lg-3">
<h2 class="h6 fw-semibold mb-1 text-break">{{ edited_user.username }}</h2>
<p class="small text-body-secondary mb-0">{% trans "How the user signs in." %}</p>
<p class="small text-body-secondary mb-0">{% trans "How the user signs in." %} {% trans "Changing it deletes the user's API tokens." %}</p>
</div>
<div class="col-lg-9 form-section-fields">
{% trans "Signs in with the password set here." as with_help %}

View file

@ -2,7 +2,7 @@
user form says what its fields mean, and a password set by an administrator
can be one the user must change."""
from accounts.models import UserAttributes
from accounts.models import ApiToken, UserAttributes
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
@ -140,6 +140,15 @@ class UserPagesTests(TestCase):
self.client.post(url, {"password1": "New-pw-4567y", "password2": "New-pw-4567y"})
self.assertFalse(UserAttributes.objects.get(user=self.alice).must_change_password)
def test_a_password_set_by_an_administrator_ends_the_users_api_tokens(self):
# a reset closes a taken-over account: a stolen token must not outlive it
ApiToken.objects.create(self.alice, expiry=None, name="script")
ApiToken.objects.create(self.admin, expiry=None, name="mine")
url = reverse("admin:user_update_password", args=[self.alice.pk])
self.client.post(url, {"password1": "New-pw-4567x", "password2": "New-pw-4567x"})
self.assertFalse(ApiToken.objects.filter(user=self.alice).exists())
self.assertTrue(ApiToken.objects.filter(user=self.admin).exists())
def test_a_user_without_a_password_cannot_be_made_to_change_it(self):
# the change form asks for the old password: one they never had
url = reverse("admin:user_update_password", args=[self.alice.pk])

View file

@ -1,6 +1,6 @@
from datetime import timedelta
from accounts.models import Instance, UserAttributes, UserInstance
from accounts.models import ApiToken, Instance, UserAttributes, UserInstance
from appsettings.settings import app_settings
from django.conf import settings
from django.contrib import messages
@ -208,6 +208,8 @@ def user_update_password(request, pk):
user = form.save()
UserAttributes.objects.update_or_create(
user=user, defaults={"must_change_password": form.cleaned_data["must_change_password"]})
# a reset closes a taken-over account: its tokens would outlive the old password
ApiToken.objects.filter(user=user).delete()
update_session_auth_hash(request, user) # Important!
messages.success(
request, _("Password changed for %(user)s") % {"user": user.username}