1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +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

@ -810,7 +810,7 @@ The REST API (Django REST Framework, documented with `drf-spectacular`) takes th
* **Swagger UI:** `http://<server>/swagger/`
* **ReDoc UI:** `http://<server>/redoc/`
* **OpenAPI 3.0 schema:** `http://<server>/api/schema/` (JSON or YAML)
* **API tokens**, for scripts and tools: **Profile → API Tokens**. A token acts as its user and is shown once; send it as a header:
* **API tokens**, for scripts and tools: **Profile → API Tokens**. A token acts as its user and is shown once; an administrator who sets the user's password deletes them. Send it as a header:
```bash
curl -H "Authorization: Token wvc_..." https://<server>/api/v1/instances/

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}

Binary file not shown.

View file

@ -913,16 +913,16 @@ msgstr "Sonuç"
msgid "IP"
msgstr "IP"
#: admin/templates/admin/logs.html:56 admin/views.py:286 instances/states.py:19
#: admin/templates/admin/logs.html:56 admin/views.py:288 instances/states.py:19
#: instances/views.py:419 instances/views.py:421 logs/views.py:17
msgid "Unknown"
msgstr "Bilinmeyen"
#: admin/templates/admin/logs.html:56 admin/views.py:286 logs/views.py:17
#: admin/templates/admin/logs.html:56 admin/views.py:288 logs/views.py:17
msgid "Done"
msgstr "Tamamlandı"
#: admin/templates/admin/logs.html:56 admin/views.py:286 logs/views.py:17
#: admin/templates/admin/logs.html:56 admin/views.py:288 logs/views.py:17
#: operations/templates/operations/job.html:107
msgid "Failed"
msgstr "Başarısız"
@ -1106,6 +1106,10 @@ msgstr "Engeli Kaldır"
msgid "How the user signs in."
msgstr "Kullanıcının nasıl oturum açtığı."
#: admin/templates/admin/user_password_form.html:19
msgid "Changing it deletes the user's API tokens."
msgstr "Değiştirmek kullanıcının API token'larını siler."
#: admin/templates/admin/user_password_form.html:22
msgid "Signs in with the password set here."
msgstr "Burada belirlenen parolayla oturum açar."
@ -1138,27 +1142,27 @@ msgstr "Kullanıcı Oluştur"
msgid "Update User"
msgstr "Kullanıcı Güncelle"
#: admin/views.py:213
#: admin/views.py:215
#, python-format
msgid "Password changed for %(user)s"
msgstr "%(user)s parolası değiştirildi"
#: admin/views.py:237
#: admin/views.py:239
msgid "Their access to VMs, SSH keys and settings go; the VMs themselves stay."
msgstr ""
"Sanal makinelere erişimi, SSH anahtarları ve ayarları silinir; sanal "
"makinelerin kendisi kalır."
#: admin/views.py:335
#: admin/views.py:337
msgid "deleted user"
msgstr "silinmiş kullanıcı"
#: admin/views.py:346
#: admin/views.py:348
#, python-format
msgid "Removed VM %(name)s deleted"
msgstr "Kaldırılan sanal makine %(name)s silindi"
#: admin/views.py:353
#: admin/views.py:355
msgid "Its owners no longer get the VM back if its UUID shows up again."
msgstr ""
"UUID'si yeniden görünse bile sahipleri sanal makineyi artık geri almaz."

Binary file not shown.

View file

@ -893,16 +893,16 @@ msgstr "结果"
msgid "IP"
msgstr "IP"
#: admin/templates/admin/logs.html:56 admin/views.py:286 instances/states.py:19
#: admin/templates/admin/logs.html:56 admin/views.py:288 instances/states.py:19
#: instances/views.py:419 instances/views.py:421 logs/views.py:17
msgid "Unknown"
msgstr "未知"
#: admin/templates/admin/logs.html:56 admin/views.py:286 logs/views.py:17
#: admin/templates/admin/logs.html:56 admin/views.py:288 logs/views.py:17
msgid "Done"
msgstr "完成"
#: admin/templates/admin/logs.html:56 admin/views.py:286 logs/views.py:17
#: admin/templates/admin/logs.html:56 admin/views.py:288 logs/views.py:17
#: operations/templates/operations/job.html:107
msgid "Failed"
msgstr "失败"
@ -1079,6 +1079,10 @@ msgstr "解除禁用"
msgid "How the user signs in."
msgstr "用户的登录方式。"
#: admin/templates/admin/user_password_form.html:19
msgid "Changing it deletes the user's API tokens."
msgstr "更改密码会删除该用户的 API 令牌。"
#: admin/templates/admin/user_password_form.html:22
msgid "Signs in with the password set here."
msgstr "使用此处设置的密码登录。"
@ -1111,25 +1115,25 @@ msgstr "创建用户"
msgid "Update User"
msgstr "更新用户"
#: admin/views.py:213
#: admin/views.py:215
#, python-format
msgid "Password changed for %(user)s"
msgstr "已为 %(user)s 修改密码"
#: admin/views.py:237
#: admin/views.py:239
msgid "Their access to VMs, SSH keys and settings go; the VMs themselves stay."
msgstr "其对虚拟机、SSH 密钥和设置的访问将被移除;虚拟机本身保留。"
#: admin/views.py:335
#: admin/views.py:337
msgid "deleted user"
msgstr "已删除的用户"
#: admin/views.py:346
#: admin/views.py:348
#, python-format
msgid "Removed VM %(name)s deleted"
msgstr "已删除已移除的虚拟机 %(name)s"
#: admin/views.py:353
#: admin/views.py:355
msgid "Its owners no longer get the VM back if its UUID shows up again."
msgstr "若其 UUID 再次出现,其所有者将不再恢复对该虚拟机的所有权。"