diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index ba456b038..d490e8fe9 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -70,6 +70,11 @@ class TokenSerializer(ValidatedModelSerializer): return super().to_internal_value(data) +class TokenProvisionSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField() + + class ObjectPermissionSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') object_types = ContentTypeField( diff --git a/netbox/users/api/urls.py b/netbox/users/api/urls.py index 43d960980..15e4d1530 100644 --- a/netbox/users/api/urls.py +++ b/netbox/users/api/urls.py @@ -1,3 +1,5 @@ +from django.urls import include, path + from netbox.api import OrderedDefaultRouter from . import views @@ -19,4 +21,7 @@ router.register('permissions', views.ObjectPermissionViewSet) router.register('config', views.UserConfigViewSet, basename='userconfig') app_name = 'users-api' -urlpatterns = router.urls +urlpatterns = [ + path('tokens/provision/', views.TokenProvisionView.as_view(), name='token_provision'), + path('', include(router.urls)), +] diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index a1a8728a3..a0a7d8ed6 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -1,8 +1,12 @@ +from django.contrib.auth import authenticate from django.contrib.auth.models import Group, User from django.db.models import Count +from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.routers import APIRootView +from rest_framework.status import HTTP_201_CREATED +from rest_framework.views import APIView from rest_framework.viewsets import ViewSet from netbox.api.views import ModelViewSet @@ -56,6 +60,34 @@ class TokenViewSet(ModelViewSet): return queryset.filter(user=self.request.user) +class TokenProvisionView(APIView): + """ + Non-authenticated REST API endpoint via which a user may create a Token. + """ + permission_classes = [] + swagger_schema = None # TODO: Generate a schema + + def post(self, request): + serializer = serializers.TokenProvisionSerializer(data=request.data) + serializer.is_valid() + + # Authenticate the user account based on the provided credentials + user = authenticate( + request=request, + username=serializer.data['username'], + password=serializer.data['password'] + ) + if user is None: + raise AuthenticationFailed("Invalid username/password") + + # Create a new Token for the User + token = Token(user=user) + token.save() + data = serializers.TokenSerializer(token, context={'request': request}).data + + return Response(data, status=HTTP_201_CREATED) + + # # ObjectPermissions # diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index c63da0639..9ddb76884 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -106,6 +106,37 @@ class TokenTest(APIViewTestCases.APIViewTestCase): }, ] + def test_provision_token_valid(self): + """ + Test the provisioning of a new REST API token given a valid username and password. + """ + data = { + 'username': 'user1', + 'password': 'abc123', + } + user = User.objects.create_user(**data) + url = reverse('users-api:token_provision') + + response = self.client.post(url, **self.header, data=data) + self.assertEqual(response.status_code, 201) + self.assertIn('key', response.data) + self.assertEqual(len(response.data['key']), 40) + token = Token.objects.get(user=user) + self.assertEqual(token.key, response.data['key']) + + def test_provision_token_invalid(self): + """ + Test the behavior of the token provisioning view when invalid credentials are supplied. + """ + data = { + 'username': 'nonexistentuser', + 'password': 'abc123', + } + url = reverse('users-api:token_provision') + + response = self.client.post(url, **self.header, data=data) + self.assertEqual(response.status_code, 403) + class ObjectPermissionTest(APIViewTestCases.APIViewTestCase): model = ObjectPermission