Преглед на файлове

Introduce ObjectView to enforce object-level permissions for individual object views

Jeremy Stretch преди 5 години
родител
ревизия
e61fc1f709
променени са 9 файла, в които са добавени 118 реда и са изтрити 106 реда
  1. 4 0
      docs/development/utility-views.md
  2. 7 9
      netbox/circuits/views.py
  3. 41 39
      netbox/dcim/views.py
  4. 10 10
      netbox/extras/views.py
  5. 27 30
      netbox/ipam/views.py
  6. 5 4
      netbox/secrets/views.py
  7. 4 6
      netbox/tenancy/views.py
  8. 12 0
      netbox/utilities/views.py
  9. 8 8
      netbox/virtualization/views.py

+ 4 - 0
docs/development/utility-views.md

@@ -4,6 +4,10 @@ Utility views are reusable views that handle common CRUD tasks, such as listing
 
 
 ## Individual Views
 ## Individual Views
 
 
+### ObjectView
+
+Retrieve and display a single object.
+
 ### ObjectListView
 ### ObjectListView
 
 
 Generates a paginated table of objects from a given queryset, which may optionally be filtered.
 Generates a paginated table of objects from a given queryset, which may optionally be filtered.

+ 7 - 9
netbox/circuits/views.py

@@ -1,18 +1,16 @@
 from django.conf import settings
 from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.decorators import permission_required
-from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 from django.db import transaction
 from django.db.models import Count, OuterRef
 from django.db.models import Count, OuterRef
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
-from django.views.generic import View
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
 from extras.models import Graph
 from extras.models import Graph
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
 from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
+    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .choices import CircuitTerminationSideChoices
 from .choices import CircuitTerminationSideChoices
@@ -30,12 +28,12 @@ class ProviderListView(ObjectListView):
     table = tables.ProviderTable
     table = tables.ProviderTable
 
 
 
 
-class ProviderView(PermissionRequiredMixin, View):
-    permission_required = 'circuits.view_provider'
+class ProviderView(ObjectView):
+    queryset = Provider.objects.all()
 
 
     def get(self, request, slug):
     def get(self, request, slug):
 
 
-        provider = get_object_or_404(Provider, slug=slug)
+        provider = get_object_or_404(self.queryset, slug=slug)
         circuits = Circuit.objects.filter(
         circuits = Circuit.objects.filter(
             provider=provider
             provider=provider
         ).prefetch_related(
         ).prefetch_related(
@@ -135,12 +133,12 @@ class CircuitListView(ObjectListView):
     table = tables.CircuitTable
     table = tables.CircuitTable
 
 
 
 
-class CircuitView(PermissionRequiredMixin, View):
-    permission_required = 'circuits.view_circuit'
+class CircuitView(ObjectView):
+    queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant__group')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        circuit = get_object_or_404(Circuit.objects.prefetch_related('provider', 'type', 'tenant__group'), pk=pk)
+        circuit = get_object_or_404(self.queryset, pk=pk)
         termination_a = CircuitTermination.objects.prefetch_related(
         termination_a = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
             'site__region', 'connected_endpoint__device'
         ).filter(
         ).filter(

+ 41 - 39
netbox/dcim/views.py

@@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
 from utilities.utils import csv_format
 from utilities.utils import csv_format
 from utilities.views import (
 from utilities.views import (
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
-    ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
+    ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
@@ -185,8 +185,7 @@ class SiteListView(ObjectListView):
     table = tables.SiteTable
     table = tables.SiteTable
 
 
 
 
-class SiteView(ObjectPermissionRequiredMixin, View):
-    permission_required = 'dcim.view_site'
+class SiteView(ObjectView):
     queryset = Site.objects.prefetch_related('region', 'tenant__group')
     queryset = Site.objects.prefetch_related('region', 'tenant__group')
 
 
     def get(self, request, slug):
     def get(self, request, slug):
@@ -362,12 +361,12 @@ class RackElevationListView(PermissionRequiredMixin, View):
         })
         })
 
 
 
 
-class RackView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_rack'
+class RackView(ObjectView):
+    queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        rack = get_object_or_404(Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
+        rack = get_object_or_404(self.queryset, pk=pk)
 
 
         nonracked_devices = Device.objects.filter(
         nonracked_devices = Device.objects.filter(
             rack=rack,
             rack=rack,
@@ -440,12 +439,12 @@ class RackReservationListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class RackReservationView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_rackreservation'
+class RackReservationView(ObjectView):
+    queryset = RackReservation.objects.prefetch_related('rack')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        rackreservation = get_object_or_404(RackReservation.objects.prefetch_related('rack'), pk=pk)
+        rackreservation = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'dcim/rackreservation.html', {
         return render(request, 'dcim/rackreservation.html', {
             'rackreservation': rackreservation,
             'rackreservation': rackreservation,
@@ -546,12 +545,12 @@ class DeviceTypeListView(ObjectListView):
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
 
 
 
 
-class DeviceTypeView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_devicetype'
+class DeviceTypeView(ObjectView):
+    queryset = DeviceType.objects.prefetch_related('manufacturer')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        devicetype = get_object_or_404(DeviceType, pk=pk)
+        devicetype = get_object_or_404(self.queryset, pk=pk)
 
 
         # Component tables
         # Component tables
         consoleport_table = tables.ConsolePortTemplateTable(
         consoleport_table = tables.ConsolePortTemplateTable(
@@ -990,14 +989,14 @@ class DeviceListView(ObjectListView):
     template_name = 'dcim/device_list.html'
     template_name = 'dcim/device_list.html'
 
 
 
 
-class DeviceView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_device'
+class DeviceView(ObjectView):
+    queryset = Device.objects.prefetch_related(
+        'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
+    )
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        device = get_object_or_404(Device.objects.prefetch_related(
-            'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
-        ), pk=pk)
+        device = get_object_or_404(self.queryset, pk=pk)
 
 
         # VirtualChassis members
         # VirtualChassis members
         if device.virtual_chassis is not None:
         if device.virtual_chassis is not None:
@@ -1068,12 +1067,12 @@ class DeviceView(PermissionRequiredMixin, View):
         })
         })
 
 
 
 
-class DeviceInventoryView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_device'
+class DeviceInventoryView(ObjectView):
+    queryset = Device.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        device = get_object_or_404(Device, pk=pk)
+        device = get_object_or_404(self.queryset, pk=pk)
         inventory_items = InventoryItem.objects.filter(
         inventory_items = InventoryItem.objects.filter(
             device=device, parent=None
             device=device, parent=None
         ).prefetch_related(
         ).prefetch_related(
@@ -1087,12 +1086,13 @@ class DeviceInventoryView(PermissionRequiredMixin, View):
         })
         })
 
 
 
 
-class DeviceStatusView(PermissionRequiredMixin, View):
+class DeviceStatusView(ObjectView):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
+    queryset = Device.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        device = get_object_or_404(Device, pk=pk)
+        device = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'dcim/device_status.html', {
         return render(request, 'dcim/device_status.html', {
             'device': device,
             'device': device,
@@ -1102,10 +1102,11 @@ class DeviceStatusView(PermissionRequiredMixin, View):
 
 
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
+    queryset = Device.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        device = get_object_or_404(Device, pk=pk)
+        device = get_object_or_404(self.queryset, pk=pk)
         interfaces = device.vc_interfaces.exclude(type__in=NONCONNECTABLE_IFACE_TYPES).prefetch_related(
         interfaces = device.vc_interfaces.exclude(type__in=NONCONNECTABLE_IFACE_TYPES).prefetch_related(
             '_connected_interface__device'
             '_connected_interface__device'
         )
         )
@@ -1119,10 +1120,11 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 
 
 class DeviceConfigView(PermissionRequiredMixin, View):
 class DeviceConfigView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
+    queryset = Device.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        device = get_object_or_404(Device, pk=pk)
+        device = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'dcim/device_config.html', {
         return render(request, 'dcim/device_config.html', {
             'device': device,
             'device': device,
@@ -1426,12 +1428,12 @@ class InterfaceListView(ObjectListView):
     action_buttons = ('import', 'export')
     action_buttons = ('import', 'export')
 
 
 
 
-class InterfaceView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_interface'
+class InterfaceView(ObjectView):
+    queryset = Interface.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        interface = get_object_or_404(Interface, pk=pk)
+        interface = get_object_or_404(self.queryset, pk=pk)
 
 
         # Get assigned IP addresses
         # Get assigned IP addresses
         ipaddress_table = InterfaceIPAddressTable(
         ipaddress_table = InterfaceIPAddressTable(
@@ -1878,12 +1880,12 @@ class CableListView(ObjectListView):
     action_buttons = ('import', 'export')
     action_buttons = ('import', 'export')
 
 
 
 
-class CableView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_cable'
+class CableView(ObjectView):
+    queryset = Cable.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        cable = get_object_or_404(Cable, pk=pk)
+        cable = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'dcim/cable.html', {
         return render(request, 'dcim/cable.html', {
             'cable': cable,
             'cable': cable,
@@ -2194,11 +2196,11 @@ class VirtualChassisListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class VirtualChassisView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_virtualchassis'
+class VirtualChassisView(ObjectView):
+    queryset = VirtualChassis.objects.prefetch_related('members')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
-        virtualchassis = get_object_or_404(VirtualChassis.objects.prefetch_related('members'), pk=pk)
+        virtualchassis = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'dcim/virtualchassis.html', {
         return render(request, 'dcim/virtualchassis.html', {
             'virtualchassis': virtualchassis,
             'virtualchassis': virtualchassis,
@@ -2461,12 +2463,12 @@ class PowerPanelListView(ObjectListView):
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
 
 
 
 
-class PowerPanelView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_powerpanel'
+class PowerPanelView(ObjectView):
+    queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        powerpanel = get_object_or_404(PowerPanel.objects.prefetch_related('site', 'rack_group'), pk=pk)
+        powerpanel = get_object_or_404(self.queryset, pk=pk)
         powerfeed_table = tables.PowerFeedTable(
         powerfeed_table = tables.PowerFeedTable(
             data=PowerFeed.objects.filter(power_panel=powerpanel).prefetch_related('rack'),
             data=PowerFeed.objects.filter(power_panel=powerpanel).prefetch_related('rack'),
             orderable=False
             orderable=False
@@ -2529,12 +2531,12 @@ class PowerFeedListView(ObjectListView):
     table = tables.PowerFeedTable
     table = tables.PowerFeedTable
 
 
 
 
-class PowerFeedView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.view_powerfeed'
+class PowerFeedView(ObjectView):
+    queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        powerfeed = get_object_or_404(PowerFeed.objects.prefetch_related('power_panel', 'rack'), pk=pk)
+        powerfeed = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'dcim/powerfeed.html', {
         return render(request, 'dcim/powerfeed.html', {
             'powerfeed': powerfeed,
             'powerfeed': powerfeed,

+ 10 - 10
netbox/extras/views.py

@@ -13,7 +13,7 @@ from django_tables2 import RequestConfig
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.utils import shallow_compare_dict
 from utilities.utils import shallow_compare_dict
-from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
+from utilities.views import BulkDeleteView, BulkEditView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView
 from . import filters, forms
 from . import filters, forms
 from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
 from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
 from .reports import get_report, get_reports
 from .reports import get_report, get_reports
@@ -37,12 +37,12 @@ class TagListView(ObjectListView):
     action_buttons = ()
     action_buttons = ()
 
 
 
 
-class TagView(PermissionRequiredMixin, View):
-    permission_required = 'extras.view_tag'
+class TagView(ObjectView):
+    queryset = Tag.objects.all()
 
 
     def get(self, request, slug):
     def get(self, request, slug):
 
 
-        tag = get_object_or_404(Tag, slug=slug)
+        tag = get_object_or_404(self.queryset, slug=slug)
         tagged_items = TaggedItem.objects.filter(
         tagged_items = TaggedItem.objects.filter(
             tag=tag
             tag=tag
         ).prefetch_related(
         ).prefetch_related(
@@ -109,11 +109,11 @@ class ConfigContextListView(ObjectListView):
     action_buttons = ('add',)
     action_buttons = ('add',)
 
 
 
 
-class ConfigContextView(PermissionRequiredMixin, View):
-    permission_required = 'extras.view_configcontext'
+class ConfigContextView(ObjectView):
+    queryset = ConfigContext.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
-        configcontext = get_object_or_404(ConfigContext, pk=pk)
+        configcontext = get_object_or_404(self.queryset, pk=pk)
 
 
         # Determine user's preferred output format
         # Determine user's preferred output format
         if request.GET.get('format') in ['json', 'yaml']:
         if request.GET.get('format') in ['json', 'yaml']:
@@ -195,12 +195,12 @@ class ObjectChangeListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class ObjectChangeView(PermissionRequiredMixin, View):
-    permission_required = 'extras.view_objectchange'
+class ObjectChangeView(ObjectView):
+    queryset = ObjectChange.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        objectchange = get_object_or_404(ObjectChange, pk=pk)
+        objectchange = get_object_or_404(self.queryset, pk=pk)
 
 
         related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk)
         related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk)
         related_changes_table = ObjectChangeTable(
         related_changes_table = ObjectChangeTable(

+ 27 - 30
netbox/ipam/views.py

@@ -10,8 +10,8 @@ from django_tables2 import RequestConfig
 from dcim.models import Device, Interface
 from dcim.models import Device, Interface
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
 from utilities.views import (
-    BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
-    ObjectPermissionRequiredMixin,
+    BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
+    ObjectListView,
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
@@ -120,12 +120,12 @@ class VRFListView(ObjectListView):
     table = tables.VRFTable
     table = tables.VRFTable
 
 
 
 
-class VRFView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_vrf'
+class VRFView(ObjectView):
+    queryset = VRF.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        vrf = get_object_or_404(VRF.objects.all(), pk=pk)
+        vrf = get_object_or_404(self.queryset, pk=pk)
         prefix_count = Prefix.objects.filter(vrf=vrf).count()
         prefix_count = Prefix.objects.filter(vrf=vrf).count()
 
 
         return render(request, 'ipam/vrf.html', {
         return render(request, 'ipam/vrf.html', {
@@ -298,12 +298,12 @@ class AggregateListView(ObjectListView):
         }
         }
 
 
 
 
-class AggregateView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_aggregate'
+class AggregateView(ObjectView):
+    queryset = Aggregate.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        aggregate = get_object_or_404(Aggregate, pk=pk)
+        aggregate = get_object_or_404(self.queryset, pk=pk)
 
 
         # Find all child prefixes contained by this aggregate
         # Find all child prefixes contained by this aggregate
         child_prefixes = Prefix.objects.filter(
         child_prefixes = Prefix.objects.filter(
@@ -422,8 +422,7 @@ class PrefixListView(ObjectListView):
         return self.queryset.annotate_depth(limit=limit)
         return self.queryset.annotate_depth(limit=limit)
 
 
 
 
-class PrefixView(ObjectPermissionRequiredMixin, View):
-    permission_required = 'ipam.view_prefix'
+class PrefixView(ObjectView):
     queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
     queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
@@ -465,12 +464,12 @@ class PrefixView(ObjectPermissionRequiredMixin, View):
         })
         })
 
 
 
 
-class PrefixPrefixesView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_prefix'
+class PrefixPrefixesView(ObjectView):
+    queryset = Prefix.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
+        prefix = get_object_or_404(self.queryset, pk=pk)
 
 
         # Child prefixes table
         # Child prefixes table
         child_prefixes = prefix.get_child_prefixes().prefetch_related(
         child_prefixes = prefix.get_child_prefixes().prefetch_related(
@@ -509,12 +508,12 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
         })
         })
 
 
 
 
-class PrefixIPAddressesView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_prefix'
+class PrefixIPAddressesView(ObjectView):
+    queryset = Prefix.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
+        prefix = get_object_or_404(self.queryset, pk=pk)
 
 
         # Find all IPAddresses belonging to this Prefix
         # Find all IPAddresses belonging to this Prefix
         ipaddresses = prefix.get_child_ips().prefetch_related(
         ipaddresses = prefix.get_child_ips().prefetch_related(
@@ -601,12 +600,12 @@ class IPAddressListView(ObjectListView):
     table = tables.IPAddressDetailTable
     table = tables.IPAddressDetailTable
 
 
 
 
-class IPAddressView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_ipaddress'
+class IPAddressView(ObjectView):
+    queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        ipaddress = get_object_or_404(IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), pk=pk)
+        ipaddress = get_object_or_404(self.queryset, pk=pk)
 
 
         # Parent prefixes table
         # Parent prefixes table
         parent_prefixes = Prefix.objects.filter(
         parent_prefixes = Prefix.objects.filter(
@@ -833,14 +832,12 @@ class VLANListView(ObjectListView):
     table = tables.VLANDetailTable
     table = tables.VLANDetailTable
 
 
 
 
-class VLANView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_vlan'
+class VLANView(ObjectView):
+    queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        vlan = get_object_or_404(VLAN.objects.prefetch_related(
-            'site__region', 'tenant__group', 'role'
-        ), pk=pk)
+        vlan = get_object_or_404(self.queryset, pk=pk)
         prefixes = Prefix.objects.filter(vlan=vlan).prefetch_related('vrf', 'site', 'role')
         prefixes = Prefix.objects.filter(vlan=vlan).prefetch_related('vrf', 'site', 'role')
         prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
         prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
         prefix_table.exclude = ('vlan',)
         prefix_table.exclude = ('vlan',)
@@ -851,12 +848,12 @@ class VLANView(PermissionRequiredMixin, View):
         })
         })
 
 
 
 
-class VLANMembersView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_vlan'
+class VLANMembersView(ObjectView):
+    queryset = VLAN.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
+        vlan = get_object_or_404(self.queryset, pk=pk)
         members = vlan.get_members().prefetch_related('device', 'virtual_machine')
         members = vlan.get_members().prefetch_related('device', 'virtual_machine')
 
 
         members_table = tables.VLANMemberTable(members)
         members_table = tables.VLANMemberTable(members)
@@ -920,12 +917,12 @@ class ServiceListView(ObjectListView):
     action_buttons = ('export',)
     action_buttons = ('export',)
 
 
 
 
-class ServiceView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_service'
+class ServiceView(ObjectView):
+    queryset = Service.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        service = get_object_or_404(Service, pk=pk)
+        service = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'ipam/service.html', {
         return render(request, 'ipam/service.html', {
             'service': service,
             'service': service,

+ 5 - 4
netbox/secrets/views.py

@@ -9,7 +9,8 @@ from django.urls import reverse
 from django.views.generic import View
 from django.views.generic import View
 
 
 from utilities.views import (
 from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, GetReturnURLMixin, ObjectDeleteView, ObjectEditView, ObjectListView,
+    BulkDeleteView, BulkEditView, BulkImportView, GetReturnURLMixin, ObjectView, ObjectDeleteView, ObjectEditView,
+    ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .decorators import userkey_required
 from .decorators import userkey_required
@@ -66,12 +67,12 @@ class SecretListView(ObjectListView):
     action_buttons = ('import', 'export')
     action_buttons = ('import', 'export')
 
 
 
 
-class SecretView(PermissionRequiredMixin, View):
-    permission_required = 'secrets.view_secret'
+class SecretView(ObjectView):
+    queryset = Secret.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        secret = get_object_or_404(Secret, pk=pk)
+        secret = get_object_or_404(self.queryset, pk=pk)
 
 
         return render(request, 'secrets/secret.html', {
         return render(request, 'secrets/secret.html', {
             'secret': secret,
             'secret': secret,

+ 4 - 6
netbox/tenancy/views.py

@@ -1,13 +1,11 @@
-from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, render
 from django.shortcuts import get_object_or_404, render
-from django.views.generic import View
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
 from dcim.models import Site, Rack, Device, RackReservation
 from dcim.models import Site, Rack, Device, RackReservation
 from ipam.models import IPAddress, Prefix, VLAN, VRF
 from ipam.models import IPAddress, Prefix, VLAN, VRF
 from utilities.views import (
 from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
+    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from virtualization.models import VirtualMachine, Cluster
 from virtualization.models import VirtualMachine, Cluster
 from . import filters, forms, tables
 from . import filters, forms, tables
@@ -59,12 +57,12 @@ class TenantListView(ObjectListView):
     table = tables.TenantTable
     table = tables.TenantTable
 
 
 
 
-class TenantView(PermissionRequiredMixin, View):
-    permission_required = 'tenancy.view_tenant'
+class TenantView(ObjectView):
+    queryset = Tenant.objects.prefetch_related('group')
 
 
     def get(self, request, slug):
     def get(self, request, slug):
 
 
-        tenant = get_object_or_404(Tenant, slug=slug)
+        tenant = get_object_or_404(self.queryset, slug=slug)
         stats = {
         stats = {
             'site_count': Site.objects.filter(tenant=tenant).count(),
             'site_count': Site.objects.filter(tenant=tenant).count(),
             'rack_count': Rack.objects.filter(tenant=tenant).count(),
             'rack_count': Rack.objects.filter(tenant=tenant).count(),

+ 12 - 0
netbox/utilities/views.py

@@ -118,6 +118,18 @@ class GetReturnURLMixin(object):
 # Generic views
 # Generic views
 #
 #
 
 
+class ObjectView(ObjectPermissionRequiredMixin, View):
+    """
+    Retrieve a single object for display.
+
+    :param queryset: The base queryset for retrieving the object.
+    """
+    queryset = None
+
+    def get_required_permission(self):
+        return get_permission_for_model(self.queryset.model, 'view')
+
+
 class ObjectListView(ObjectPermissionRequiredMixin, View):
 class ObjectListView(ObjectPermissionRequiredMixin, View):
     """
     """
     List a series of objects.
     List a series of objects.

+ 8 - 8
netbox/virtualization/views.py

@@ -11,8 +11,8 @@ from dcim.tables import DeviceTable
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
 from ipam.models import Service
 from ipam.models import Service
 from utilities.views import (
 from utilities.views import (
-    BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectDeleteView,
-    ObjectEditView, ObjectListView,
+    BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectView,
+    ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -85,12 +85,12 @@ class ClusterListView(ObjectListView):
     filterset_form = forms.ClusterFilterForm
     filterset_form = forms.ClusterFilterForm
 
 
 
 
-class ClusterView(PermissionRequiredMixin, View):
-    permission_required = 'virtualization.view_cluster'
+class ClusterView(ObjectView):
+    queryset = Cluster.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        cluster = get_object_or_404(Cluster, pk=pk)
+        cluster = get_object_or_404(self.queryset, pk=pk)
         devices = Device.objects.filter(cluster=cluster).prefetch_related(
         devices = Device.objects.filter(cluster=cluster).prefetch_related(
             'site', 'rack', 'tenant', 'device_type__manufacturer'
             'site', 'rack', 'tenant', 'device_type__manufacturer'
         )
         )
@@ -233,12 +233,12 @@ class VirtualMachineListView(ObjectListView):
     template_name = 'virtualization/virtualmachine_list.html'
     template_name = 'virtualization/virtualmachine_list.html'
 
 
 
 
-class VirtualMachineView(PermissionRequiredMixin, View):
-    permission_required = 'virtualization.view_virtualmachine'
+class VirtualMachineView(ObjectView):
+    queryset = VirtualMachine.objects.prefetch_related('tenant__group')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        virtualmachine = get_object_or_404(VirtualMachine.objects.prefetch_related('tenant__group'), pk=pk)
+        virtualmachine = get_object_or_404(self.queryset, pk=pk)
         interfaces = Interface.objects.filter(virtual_machine=virtualmachine)
         interfaces = Interface.objects.filter(virtual_machine=virtualmachine)
         services = Service.objects.filter(virtual_machine=virtualmachine)
         services = Service.objects.filter(virtual_machine=virtualmachine)