1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Add object permission support for create/update/delete API views

This commit is contained in:
Jeremy Stretch
2020-05-21 10:51:40 -04:00
parent fa8407371b
commit a928d337d9
2 changed files with 138 additions and 56 deletions

View File

@ -4,7 +4,8 @@ from collections import OrderedDict
import pytz
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.db.models import ManyToManyField, ProtectedError
from django.urls import reverse
from rest_framework.exceptions import APIException
@ -14,6 +15,7 @@ from rest_framework.response import Response
from rest_framework.serializers import Field, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet as _ModelViewSet
from netbox.api import TokenPermissions
from users.models import ObjectPermission
from .utils import dict_to_filter_params, dynamic_import
@ -329,11 +331,13 @@ class ModelViewSet(_ModelViewSet):
if not request.user.is_authenticated or request.user.is_superuser:
return
# Determine the required permission
permission_required = "{}.view_{}".format(
self.queryset.model._meta.app_label,
self.queryset.model._meta.model_name
)
# TODO: Move this to a cleaner function
# Determine the required permission based on the request method
kwargs = {
'app_label': self.queryset.model._meta.app_label,
'model_name': self.queryset.model._meta.model_name
}
permission_required = TokenPermissions.perms_map[request.method][0] % kwargs
# Enforce object-level permissions
if permission_required not in {*request.user._user_perm_cache, *request.user._group_perm_cache}:
@ -361,34 +365,49 @@ class ModelViewSet(_ModelViewSet):
**kwargs
)
def list(self, *args, **kwargs):
def _validate_objects(self, instance):
"""
Call to super to allow for caching
Check that the provided instance or list of instances are matched by the current queryset. This confirms that
any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
"""
return super().list(*args, **kwargs)
def retrieve(self, *args, **kwargs):
"""
Call to super to allow for caching
"""
return super().retrieve(*args, **kwargs)
#
# Logging
#
if type(instance) is list:
# Check that all instances are still included in the view's queryset
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
if conforming_count != len(instance):
raise ObjectDoesNotExist
else:
# Check that the instance is matched by the view's queryset
self.queryset.get(pk=instance.pk)
def perform_create(self, serializer):
model = serializer.child.Meta.model if hasattr(serializer, 'many') else serializer.Meta.model
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Creating new {model._meta.verbose_name}")
return super().perform_create(serializer)
# Enforce object-level permissions on save()
try:
with transaction.atomic():
instance = serializer.save()
self._validate_objects(instance)
except ObjectDoesNotExist:
raise PermissionDenied()
def perform_update(self, serializer):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Updating {serializer.instance} (PK: {serializer.instance.pk})")
return super().perform_update(serializer)
logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
# Enforce object-level permissions on save()
try:
with transaction.atomic():
instance = serializer.save()
self._validate_objects(instance)
except ObjectDoesNotExist:
raise PermissionDenied()
def perform_destroy(self, instance):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Deleting {instance} (PK: {instance.pk})")
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
return super().perform_destroy(instance)