Repository: netbox-community/netbox

Issue:
Introduce a permission specifically to allow the creation of API tokens for other users
### NetBox version

v3.4.7

### Feature type

Change to existing functionality

### Proposed functionality

This idea was [first proposed](https://github.com/netbox-community/netbox/issues/11091#issuecomment-1382039803) by @kkthxbye-code under #11091. This permission will control whether a specific user has the ability to create API tokens on behalf of other users.

### Use case

Provides more granular control over the creation of API tokens.

### Database changes

_No response_

### External dependencies

_No response_


Discussion:

Resolving patch:
diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py
--- a/netbox/users/api/serializers.py
+++ b/netbox/users/api/serializers.py
@@ -2,6 +2,7 @@
 from django.contrib.auth.models import Group, User
 from django.contrib.contenttypes.models import ContentType
 from rest_framework import serializers
+from rest_framework.exceptions import PermissionDenied
 
 from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
 from netbox.api.serializers import ValidatedModelSerializer
@@ -91,6 +92,16 @@ def to_internal_value(self, data):
             data['key'] = Token.generate_key()
         return super().to_internal_value(data)
 
+    def validate(self, data):
+
+        # If the Token is being created on behalf of another user, enforce the grant_token permission.
+        request = self.context.get('request')
+        token_user = data.get('user')
+        if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'):
+            raise PermissionDenied("This user does not have permission to create tokens for other users.")
+
+        return super().validate(data)
+
 
 class TokenProvisionSerializer(serializers.Serializer):
     username = serializers.CharField()


Test patch:
diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py
--- a/netbox/users/tests/test_api.py
+++ b/netbox/users/tests/test_api.py
@@ -153,6 +153,26 @@ def test_provision_token_invalid(self):
         response = self.client.post(url, data, format='json', **self.header)
         self.assertEqual(response.status_code, 403)
 
+    def test_provision_token_other_user(self):
+        """
+        Test provisioning a Token for a different User with & without the grant_token permission.
+        """
+        self.add_permissions('users.add_token')
+        user2 = User.objects.create_user(username='testuser2')
+        data = {
+            'user': user2.id,
+        }
+        url = reverse('users-api:token-list')
+
+        # Attempt to create a new Token for User2 *without* the grant_token permission
+        response = self.client.post(url, data, format='json', **self.header)
+        self.assertEqual(response.status_code, 403)
+
+        # Assign grant_token permission and successfully create a new Token for User2
+        self.add_permissions('users.grant_token')
+        response = self.client.post(url, data, format='json', **self.header)
+        self.assertEqual(response.status_code, 201)
+
 
 class ObjectPermissionTest(
     # No GraphQL support for ObjectPermission