Token save fix (#12491) (#12494)

- Only update 'last_seen' field

(cherry picked from commit e918d84f60)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
github-actions[bot]
2026-07-29 07:43:26 +10:00
committed by GitHub
co-authored by Oliver
parent 8ed8c0deb5
commit 7a26ab29f5
2 changed files with 42 additions and 1 deletions
@@ -34,8 +34,10 @@ class ApiTokenAuthentication(TokenAuthentication):
if token.last_seen != datetime.date.today():
# Update the last-seen date
# Note: Use update_fields to avoid clobbering concurrent changes to
# other fields on this token (e.g. a concurrent revocation)
token.last_seen = datetime.date.today()
token.save()
token.save(update_fields=['last_seen'])
return (user, token)
+39
View File
@@ -1,6 +1,7 @@
"""API tests for various user / auth API endpoints."""
import datetime
from unittest import mock
from django.contrib.auth.models import Group, User
from django.urls import reverse
@@ -473,6 +474,44 @@ class UserTokenTests(InvenTreeAPITestCase):
self.client.get(me, expected_code=200)
def test_token_last_seen_no_clobber(self):
"""Regression test: updating token.last_seen must overwrite other fields.
Simulates a revoke landing in the window between this request's token
lookup and its last_seen save, by revoking the token (directly against
the database) from inside a patched ApiToken.save().
"""
token_key = self.get(
url=reverse('api-token'), data={'name': 'race'}, expected_code=200
).data['token']
token = ApiToken.objects.get(key=token_key)
# Force last_seen to be 'stale' so the auth backend attempts to update it
ApiToken.objects.filter(pk=token.pk).update(
last_seen=datetime.date.today() - datetime.timedelta(days=1)
)
original_save = ApiToken.save
def revoke_then_save(self, *args, **kwargs):
# Simulate a concurrent request revoking this token, via a direct
# DB write, right before this request's last_seen save lands
ApiToken.objects.filter(pk=self.pk).update(revoked=True)
return original_save(self, *args, **kwargs)
self.client.logout()
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token_key)
with mock.patch.object(ApiToken, 'save', revoke_then_save):
self.client.get(reverse('api-user-me'), expected_code=200)
token.refresh_from_db()
self.assertTrue(
token.revoked,
'Concurrent revoke must not be clobbered by the last_seen update',
)
def test_token_api(self):
"""Test the token API."""
url = reverse('api-token-list')