Просмотр исходного кода

Fixes #23558: Preserve token ownership when editing (#23589)

peio 4 часов назад
Родитель
Сommit
b34cbecc45

+ 27 - 0
netbox/account/tests.py

@@ -0,0 +1,27 @@
+from django.test import override_settings
+from django.urls import reverse
+
+from users.models import Token
+from utilities.testing import TestCase
+
+
+@override_settings(DEFAULT_PERMISSIONS={
+    'users.change_token': ({'user': '$user'},),
+})
+class UserTokenEditTestCase(TestCase):
+
+    def test_edit_own_token(self):
+        token = Token.objects.create(user=self.user, description='Original description')
+        url = reverse('account:usertoken_edit', kwargs={'pk': token.pk})
+
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(url, {
+            'description': 'Updated description',
+            'enabled': 'on',
+        })
+        self.assertEqual(response.status_code, 302)
+        token.refresh_from_db()
+        self.assertEqual(token.description, 'Updated description')
+        self.assertEqual(token.user, self.user)

+ 9 - 0
netbox/templates/users/token_edit.html

@@ -9,6 +9,15 @@
   {{ block.super }}
 {% endblock %}
 
+{% block pre_form_fields %}
+  {% if show_token_user %}
+    <div class="row mb-3">
+      <div class="col-md-3 text-md-end fw-bold">{% trans "User" %}</div>
+      <div class="col-md-9">{{ object.user }}</div>
+    </div>
+  {% endif %}
+{% endblock pre_form_fields %}
+
 {% block javascript %}
 {{ block.super }}
 <script>

+ 5 - 6
netbox/users/forms/model_forms.py

@@ -149,9 +149,8 @@ class UserTokenForm(forms.ModelForm):
         super().__init__(*args, **kwargs)
 
         if self.instance.pk:
-            # Disable the version & user fields for existing Tokens
+            # Disable the version field for existing Tokens
             self.fields['version'].disabled = True
-            self.fields['user'].disabled = True
 
         elif self.instance._state.adding:
             self.initial['version'] = TokenVersionChoices.V2
@@ -183,15 +182,15 @@ class TokenForm(UserTokenForm):
     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
+        # The owner of an existing token cannot be changed.
+        if self.instance.pk:
+            self.fields.pop('user')
 
     def clean(self):
         super().clean()
 
         # Creating a Token on behalf of another user requires the grant_token permission. This is enforced only on
-        # creation; the user field is disabled when editing an existing Token.
+        # creation; the user field is omitted when editing an existing Token.
         if self.instance._state.adding and (token_user := self.cleaned_data.get('user')):
             request = getattr(self.instance, '_request', None)
             if request is None:

+ 56 - 0
netbox/users/tests/test_views.py

@@ -264,6 +264,62 @@ class TokenTestCase(
         }
 
 
+class TokenEditOwnerTestCase(TestCase):
+    user_permissions = ('users.add_token', 'users.change_token', 'users.view_token')
+
+    def test_creation_requires_user_field(self):
+        response = self.client.get(reverse('users:token_add'))
+        self.assertIn('user', response.context['form'].fields)
+        self.assertTrue(response.context['form'].fields['user'].required)
+
+        count = Token.objects.count()
+        response = self.client.post(reverse('users:token_add'), {
+            'version': 2,
+            'description': 'Token without owner',
+            'enabled': 'on',
+        })
+        self.assertEqual(response.status_code, 200)
+        self.assertFormError(response.context['form'], 'user', 'This field is required.')
+        self.assertEqual(Token.objects.count(), count)
+
+    def test_edit_own_token_without_view_user(self):
+        token = Token.objects.create(user=self.user)
+        url = reverse('users:token_edit', kwargs={'pk': token.pk})
+        response = self.client.get(url)
+        self.assertNotIn('user', response.context['form'].fields)
+        self.assertContains(response, self.user.username)
+
+        other = create_test_user('other_owner')
+        response = self.client.post(url, {
+            'user': other.pk, 'description': 'updated', 'enabled': 'on',
+        })
+        self.assertEqual(response.status_code, 302)
+        token.refresh_from_db()
+        self.assertEqual(token.user, self.user)
+        self.assertEqual(token.description, 'updated')
+
+    def test_other_owner_visibility_and_edit(self):
+        owner = create_test_user('token_owner_unique')
+        token = Token.objects.create(user=owner)
+        url = reverse('users:token_edit', kwargs={'pk': token.pk})
+        response = self.client.get(url)
+        self.assertNotContains(response, owner.username)
+        response = self.client.post(url, {'description': 'updated', 'enabled': 'on'})
+        self.assertEqual(response.status_code, 302)
+        token.refresh_from_db()
+        self.assertEqual(token.user, owner)
+        self.assertEqual(token.description, 'updated')
+
+        self.add_permissions('users.view_user')
+        response = self.client.get(url)
+        self.assertContains(response, owner.username)
+        permission = ObjectPermission.objects.get(name='users.view_user')
+        permission.constraints = {'pk': self.user.pk}
+        permission.save()
+        response = self.client.get(url)
+        self.assertNotContains(response, owner.username)
+
+
 class TokenOneTimeAuthStringTestCase(TestCase):
     """
     Verify that the plaintext value of a newly-created Token is surfaced exactly once via the detail view, and

+ 7 - 0
netbox/users/views.py

@@ -66,6 +66,13 @@ class TokenEditView(generic.ObjectEditView):
     form = forms.TokenForm
     template_name = 'users/token_edit.html'
 
+    def get_extra_context(self, request, instance):
+        show_token_user = instance.pk and (
+            instance.user_id == request.user.pk
+            or User.objects.restrict(request.user, 'view').filter(pk=instance.user_id).exists()
+        )
+        return {'show_token_user': show_token_user}
+
     def alter_object(self, obj, request, url_args, url_kwargs):
         # Attach the request so that UserTokenForm.save() can stash the newly-generated plaintext on the session.
         obj._request = request