Token save fix (#12491)

- Only update 'last_seen' field
This commit is contained in:
Oliver
2026-07-28 23:50:34 +10:00
committed by GitHub
parent e6d1ceb20b
commit e918d84f60
2 changed files with 42 additions and 1 deletions
@@ -34,8 +34,10 @@ class ApiTokenAuthentication(TokenAuthentication):
if token.last_seen != datetime.date.today(): if token.last_seen != datetime.date.today():
# Update the last-seen date # 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.last_seen = datetime.date.today()
token.save() token.save(update_fields=['last_seen'])
return (user, token) return (user, token)
+39
View File
@@ -1,6 +1,7 @@
"""API tests for various user / auth API endpoints.""" """API tests for various user / auth API endpoints."""
import datetime import datetime
from unittest import mock
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.urls import reverse from django.urls import reverse
@@ -473,6 +474,44 @@ class UserTokenTests(InvenTreeAPITestCase):
self.client.get(me, expected_code=200) 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): def test_token_api(self):
"""Test the token API.""" """Test the token API."""
url = reverse('api-token-list') url = reverse('api-token-list')