diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 6f54a8cb0..28d9b55b6 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -584,11 +584,16 @@ Additionally, a token can be set to expire at a specific time. This can be usefu #### Client IP Restriction -!!! note - This feature was introduced in NetBox v3.3. - Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) +#### Creating Tokens for Other Users + +It is possible to provision authentication tokens for other users via the REST API. To do, so the requesting user must have the `users.grant_token` permission assigned. While all users have inherent permission to create their own tokens, this permission is required to enable the creation of tokens for other users. + +![Adding the grant action to a permission](../media/admin_ui_grant_permission.png) + +!!! warning "Exercise Caution" + The ability to create tokens on behalf of other users enables the requestor to access the created token. This ability is intended e.g. for the provisioning of tokens by automated services, and should be used with extreme caution to avoid a security compromise. ### Authenticating to the API diff --git a/docs/media/admin_ui_grant_permission.png b/docs/media/admin_ui_grant_permission.png new file mode 100644 index 000000000..2b82dcca2 Binary files /dev/null and b/docs/media/admin_ui_grant_permission.png differ diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index f1f1fc975..3194a2d28 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -2,6 +2,7 @@ from django.conf import settings 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 @@ class TokenSerializer(ValidatedModelSerializer): 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() diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 7041eb812..281f656d2 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -153,6 +153,26 @@ class TokenTest( 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