Skip to content

Commit

Permalink
Initial work on REST API endpoint for tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Jun 11, 2021
1 parent d87ec82 commit 48b4bf1
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 6 deletions.
4 changes: 4 additions & 0 deletions netbox/netbox/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def _verify_write_permission(self, request):

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
Expand Down
11 changes: 10 additions & 1 deletion netbox/users/api/nested_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
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',
]

Expand All @@ -28,6 +29,14 @@ class Meta:
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(
Expand Down
25 changes: 24 additions & 1 deletion netbox/users/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
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(
Expand Down Expand Up @@ -47,6 +55,21 @@ class Meta:
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(
Expand Down
3 changes: 3 additions & 0 deletions netbox/users/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
router.register('users', views.UserViewSet)
router.register('groups', views.GroupViewSet)

# Tokens
router.register('tokens', views.TokenViewSet)

# Permissions
router.register('permissions', views.ObjectPermissionViewSet)

Expand Down
21 changes: 20 additions & 1 deletion netbox/users/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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
Expand Down Expand Up @@ -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
#
Expand Down
13 changes: 12 additions & 1 deletion netbox/users/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.db.models import Q

from netbox.filtersets import BaseFilterSet
from users.models import ObjectPermission
from users.models import ObjectPermission, Token

__all__ = (
'GroupFilterSet',
Expand Down Expand Up @@ -60,6 +60,17 @@ def search(self, queryset, name, value):
)


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',
Expand Down
3 changes: 2 additions & 1 deletion netbox/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ def save(self, *args, **kwargs):
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()

Expand Down
34 changes: 33 additions & 1 deletion netbox/users/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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

Expand Down Expand Up @@ -75,6 +75,38 @@ def setUpTestData(cls):
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']
Expand Down

0 comments on commit 48b4bf1

Please sign in to comment.