From c914c7ae5a72dfd40fab449bab692c147bbd066f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 14 Oct 2025 13:47:23 -0400 Subject: [PATCH] Fixes #20476: Prohibit changing a token's owner --- netbox/users/api/serializers_/tokens.py | 9 ++++++++ netbox/users/forms/model_forms.py | 7 ++++++ netbox/users/tests/test_api.py | 29 ++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/netbox/users/api/serializers_/tokens.py b/netbox/users/api/serializers_/tokens.py index 3b5ec08ee34..fc0073c5bea 100644 --- a/netbox/users/api/serializers_/tokens.py +++ b/netbox/users/api/serializers_/tokens.py @@ -37,6 +37,15 @@ class Meta: read_only_fields = ('key',) brief_fields = ('id', 'url', 'display', 'version', 'key', 'write_enabled', 'description') + def get_fields(self): + fields = super().get_fields() + + # Make user field read-only if updating an existing Token. + if self.instance is not None: + fields['user'].read_only = True + + return fields + def validate(self, data): # If the Token is being created on behalf of another user, enforce the grant_token permission. diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index bcd0819a215..54857c1159b 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -177,6 +177,13 @@ class Meta(UserTokenForm.Meta): 'version', 'token', 'user', 'write_enabled', 'expires', 'description', 'allowed_ips', ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # If not creating a new Token, disable the user field + if self.instance and not self.instance._state.adding: + self.fields['user'].disabled = True + class UserForm(forms.ModelForm): password = forms.CharField( diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 741c578b6d5..597ce77de25 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -212,9 +212,9 @@ def setUp(self): @classmethod def setUpTestData(cls): users = ( - create_test_user('User1'), - create_test_user('User2'), - create_test_user('User3'), + create_test_user('User 1'), + create_test_user('User 2'), + create_test_user('User 3'), ) tokens = ( @@ -238,6 +238,10 @@ def setUpTestData(cls): }, ] + cls.update_data = { + 'description': 'Token 1', + } + def test_provision_token_valid(self): """ Test the provisioning of a new REST API token given a valid username and password. @@ -300,6 +304,25 @@ def test_provision_token_other_user(self): response = self.client.post(url, data, format='json', **self.header) self.assertEqual(response.status_code, 201) + def test_reassign_token(self): + """ + Check that a Token cannot be reassigned to another User. + """ + user1 = User.objects.get(username='User 1') + user2 = User.objects.get(username='User 2') + token1 = Token.objects.filter(user=user1).first() + self.add_permissions('users.change_token') + + data = { + 'user': user2.pk, + } + url = self._get_detail_url(token1) + response = self.client.patch(url, data, format='json', **self.header) + # Response should succeed because the read-only `user` field is ignored + self.assertEqual(response.status_code, 200) + token1.refresh_from_db() + self.assertEqual(token1.user, user1, "Token's user should not have changed") + class ObjectPermissionTest( # No GraphQL support for ObjectPermission