Răsfoiți Sursa

Initial work on REST API endpoint for tokens

jeremystretch 4 ani în urmă
părinte
comite
48b4bf1683

+ 4 - 0
netbox/netbox/api/authentication.py

@@ -59,6 +59,10 @@ class TokenPermissions(DjangoObjectPermissions):
 
     def has_permission(self, request, view):
 
+        # User must be authenticated
+        if not request.user.is_authenticated:
+            return False
+
         # Enforce Token write ability
         if isinstance(request.auth, Token) and not self._verify_write_permission(request):
             return False

+ 10 - 1
netbox/users/api/nested_serializers.py

@@ -3,11 +3,12 @@ from django.contrib.contenttypes.models import ContentType
 from rest_framework import serializers
 
 from netbox.api import ContentTypeField, WritableNestedSerializer
-from users.models import ObjectPermission
+from users.models import ObjectPermission, Token
 
 __all__ = [
     'NestedGroupSerializer',
     'NestedObjectPermissionSerializer',
+    'NestedTokenSerializer',
     'NestedUserSerializer',
 ]
 
@@ -28,6 +29,14 @@ class NestedUserSerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'display', 'username']
 
 
+class NestedTokenSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
+
+    class Meta:
+        model = Token
+        fields = ['id', 'url', 'display', 'key', 'write_enabled']
+
+
 class NestedObjectPermissionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
     object_types = ContentTypeField(

+ 24 - 1
netbox/users/api/serializers.py

@@ -3,10 +3,18 @@ from django.contrib.contenttypes.models import ContentType
 from rest_framework import serializers
 
 from netbox.api import ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
-from users.models import ObjectPermission
+from users.models import ObjectPermission, Token
 from .nested_serializers import *
 
 
+__all__ = (
+    'GroupSerializer',
+    'ObjectPermissionSerializer',
+    'TokenSerializer',
+    'UserSerializer',
+)
+
+
 class UserSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail')
     groups = SerializedPKRelatedField(
@@ -47,6 +55,21 @@ class GroupSerializer(ValidatedModelSerializer):
         fields = ('id', 'url', 'display', 'name', 'user_count')
 
 
+class TokenSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
+    key = serializers.CharField(min_length=40, max_length=40, allow_blank=True, required=False)
+    user = NestedUserSerializer()
+
+    class Meta:
+        model = Token
+        fields = ('id', 'url', 'display', 'user', 'created', 'expires', 'key', 'write_enabled', 'description')
+
+    def to_internal_value(self, data):
+        if 'key' not in data:
+            data['key'] = Token.generate_key()
+        return super().to_internal_value(data)
+
+
 class ObjectPermissionSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
     object_types = ContentTypeField(

+ 3 - 0
netbox/users/api/urls.py

@@ -9,6 +9,9 @@ router.APIRootView = views.UsersRootView
 router.register('users', views.UserViewSet)
 router.register('groups', views.GroupViewSet)
 
+# Tokens
+router.register('tokens', views.TokenViewSet)
+
 # Permissions
 router.register('permissions', views.ObjectPermissionViewSet)
 

+ 20 - 1
netbox/users/api/views.py

@@ -7,7 +7,7 @@ from rest_framework.viewsets import ViewSet
 
 from netbox.api.views import ModelViewSet
 from users import filtersets
-from users.models import ObjectPermission, UserConfig
+from users.models import ObjectPermission, Token, UserConfig
 from utilities.querysets import RestrictedQuerySet
 from utilities.utils import deepmerge
 from . import serializers
@@ -37,6 +37,25 @@ class GroupViewSet(ModelViewSet):
     filterset_class = filtersets.GroupFilterSet
 
 
+#
+# REST API tokens
+#
+
+class TokenViewSet(ModelViewSet):
+    queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
+    serializer_class = serializers.TokenSerializer
+    filterset_class = filtersets.TokenFilterSet
+
+    def get_queryset(self):
+        """
+        Limit the non-superusers to their own Tokens.
+        """
+        queryset = super().get_queryset()
+        if self.request.user.is_superuser:
+            return queryset
+        return queryset.filter(user=self.request.user)
+
+
 #
 # ObjectPermissions
 #

+ 12 - 1
netbox/users/filtersets.py

@@ -3,7 +3,7 @@ from django.contrib.auth.models import Group, User
 from django.db.models import Q
 
 from netbox.filtersets import BaseFilterSet
-from users.models import ObjectPermission
+from users.models import ObjectPermission, Token
 
 __all__ = (
     'GroupFilterSet',
@@ -60,6 +60,17 @@ class UserFilterSet(BaseFilterSet):
         )
 
 
+class TokenFilterSet(BaseFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+
+    class Meta:
+        model = Token
+        fields = ['id', 'user', 'created', 'expires', 'key', 'write_enabled']
+
+
 class ObjectPermissionFilterSet(BaseFilterSet):
     user_id = django_filters.ModelMultipleChoiceFilter(
         field_name='users',

+ 2 - 1
netbox/users/models.py

@@ -216,7 +216,8 @@ class Token(BigIDModel):
             self.key = self.generate_key()
         return super().save(*args, **kwargs)
 
-    def generate_key(self):
+    @staticmethod
+    def generate_key():
         # Generate a random 160-bit key expressed in hexadecimal.
         return binascii.hexlify(os.urandom(20)).decode()
 

+ 33 - 1
netbox/users/tests/test_api.py

@@ -2,7 +2,7 @@ from django.contrib.auth.models import Group, User
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 
-from users.models import ObjectPermission
+from users.models import ObjectPermission, Token
 from utilities.testing import APIViewTestCases, APITestCase
 from utilities.utils import deepmerge
 
@@ -75,6 +75,38 @@ class GroupTest(APIViewTestCases.APIViewTestCase):
         Group.objects.bulk_create(users)
 
 
+class TokenTest(APIViewTestCases.APIViewTestCase):
+    model = Token
+    brief_fields = ['display', 'id', 'key', 'url', 'write_enabled']
+    bulk_update_data = {
+        'description': 'New description',
+    }
+
+    def setUp(self):
+        super().setUp()
+
+        tokens = (
+            # We already start with one Token, created by the test class
+            Token(user=self.user),
+            Token(user=self.user),
+        )
+        # Use save() instead of bulk_create() to ensure keys get automatically generated
+        for token in tokens:
+            token.save()
+
+        self.create_data = [
+            {
+                'user': self.user.pk,
+            },
+            {
+                'user': self.user.pk,
+            },
+            {
+                'user': self.user.pk,
+            },
+        ]
+
+
 class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
     model = ObjectPermission
     brief_fields = ['actions', 'display', 'enabled', 'groups', 'id', 'name', 'object_types', 'url', 'users']