Просмотр исходного кода

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

Jeremy Stretch 5 лет назад
Родитель
Сommit
e61fc1f709

+ 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
 
+### ObjectView
+
+Retrieve and display a single object.
+
 ### ObjectListView
 
 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.contrib import messages
 from django.contrib.auth.decorators import permission_required
-from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 from django.db.models import Count, OuterRef
 from django.shortcuts import get_object_or_404, redirect, render
-from django.views.generic import View
 from django_tables2 import RequestConfig
 
 from extras.models import Graph
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
+    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 from . import filters, forms, tables
 from .choices import CircuitTerminationSideChoices
@@ -30,12 +28,12 @@ class ProviderListView(ObjectListView):
     table = tables.ProviderTable
 
 
-class ProviderView(PermissionRequiredMixin, View):
-    permission_required = 'circuits.view_provider'
+class ProviderView(ObjectView):
+    queryset = Provider.objects.all()
 
     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(
             provider=provider
         ).prefetch_related(
@@ -135,12 +133,12 @@ class CircuitListView(ObjectListView):
     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):
 
-        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(
             'site__region', 'connected_endpoint__device'
         ).filter(

+ 41 - 39
netbox/dcim/views.py

@@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
 from utilities.utils import csv_format
 from utilities.views import (
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
-    ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
+    ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
 )
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
@@ -185,8 +185,7 @@ class SiteListView(ObjectListView):
     table = tables.SiteTable
 
 
-class SiteView(ObjectPermissionRequiredMixin, View):
-    permission_required = 'dcim.view_site'
+class SiteView(ObjectView):
     queryset = Site.objects.prefetch_related('region', 'tenant__group')
 
     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):
 
-        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(
             rack=rack,
@@ -440,12 +439,12 @@ class RackReservationListView(ObjectListView):
     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):
 
-        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', {
             'rackreservation': rackreservation,
@@ -546,12 +545,12 @@ class DeviceTypeListView(ObjectListView):
     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):
 
-        devicetype = get_object_or_404(DeviceType, pk=pk)
+        devicetype = get_object_or_404(self.queryset, pk=pk)
 
         # Component tables
         consoleport_table = tables.ConsolePortTemplateTable(
@@ -990,14 +989,14 @@ class DeviceListView(ObjectListView):
     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):
 
-        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
         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):
 
-        device = get_object_or_404(Device, pk=pk)
+        device = get_object_or_404(self.queryset, pk=pk)
         inventory_items = InventoryItem.objects.filter(
             device=device, parent=None
         ).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')
+    queryset = Device.objects.all()
 
     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', {
             'device': device,
@@ -1102,10 +1102,11 @@ class DeviceStatusView(PermissionRequiredMixin, View):
 
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
+    queryset = Device.objects.all()
 
     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(
             '_connected_interface__device'
         )
@@ -1119,10 +1120,11 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 
 class DeviceConfigView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
+    queryset = Device.objects.all()
 
     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', {
             'device': device,
@@ -1426,12 +1428,12 @@ class InterfaceListView(ObjectListView):
     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):
 
-        interface = get_object_or_404(Interface, pk=pk)
+        interface = get_object_or_404(self.queryset, pk=pk)
 
         # Get assigned IP addresses
         ipaddress_table = InterfaceIPAddressTable(
@@ -1878,12 +1880,12 @@ class CableListView(ObjectListView):
     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):
 
-        cable = get_object_or_404(Cable, pk=pk)
+        cable = get_object_or_404(self.queryset, pk=pk)
 
         return render(request, 'dcim/cable.html', {
             'cable': cable,
@@ -2194,11 +2196,11 @@ class VirtualChassisListView(ObjectListView):
     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):
-        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', {
             'virtualchassis': virtualchassis,
@@ -2461,12 +2463,12 @@ class PowerPanelListView(ObjectListView):
     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):
 
-        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(
             data=PowerFeed.objects.filter(power_panel=powerpanel).prefetch_related('rack'),
             orderable=False
@@ -2529,12 +2531,12 @@ class PowerFeedListView(ObjectListView):
     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):
 
-        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', {
             'powerfeed': powerfeed,

+ 10 - 10
netbox/extras/views.py

@@ -13,7 +13,7 @@ from django_tables2 import RequestConfig
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 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 .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
 from .reports import get_report, get_reports
@@ -37,12 +37,12 @@ class TagListView(ObjectListView):
     action_buttons = ()
 
 
-class TagView(PermissionRequiredMixin, View):
-    permission_required = 'extras.view_tag'
+class TagView(ObjectView):
+    queryset = Tag.objects.all()
 
     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(
             tag=tag
         ).prefetch_related(
@@ -109,11 +109,11 @@ class ConfigContextListView(ObjectListView):
     action_buttons = ('add',)
 
 
-class ConfigContextView(PermissionRequiredMixin, View):
-    permission_required = 'extras.view_configcontext'
+class ConfigContextView(ObjectView):
+    queryset = ConfigContext.objects.all()
 
     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
         if request.GET.get('format') in ['json', 'yaml']:
@@ -195,12 +195,12 @@ class ObjectChangeListView(ObjectListView):
     action_buttons = ('export',)
 
 
-class ObjectChangeView(PermissionRequiredMixin, View):
-    permission_required = 'extras.view_objectchange'
+class ObjectChangeView(ObjectView):
+    queryset = ObjectChange.objects.all()
 
     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_table = ObjectChangeTable(

+ 27 - 30
netbox/ipam/views.py

@@ -10,8 +10,8 @@ from django_tables2 import RequestConfig
 from dcim.models import Device, Interface
 from utilities.paginator import EnhancedPaginator
 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 . import filters, forms, tables
@@ -120,12 +120,12 @@ class VRFListView(ObjectListView):
     table = tables.VRFTable
 
 
-class VRFView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_vrf'
+class VRFView(ObjectView):
+    queryset = VRF.objects.all()
 
     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()
 
         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):
 
-        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
         child_prefixes = Prefix.objects.filter(
@@ -422,8 +422,7 @@ class PrefixListView(ObjectListView):
         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')
 
     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):
 
-        prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
+        prefix = get_object_or_404(self.queryset, pk=pk)
 
         # Child prefixes table
         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):
 
-        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
         ipaddresses = prefix.get_child_ips().prefetch_related(
@@ -601,12 +600,12 @@ class IPAddressListView(ObjectListView):
     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):
 
-        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 = Prefix.objects.filter(
@@ -833,14 +832,12 @@ class VLANListView(ObjectListView):
     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):
 
-        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')
         prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
         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):
 
-        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_table = tables.VLANMemberTable(members)
@@ -920,12 +917,12 @@ class ServiceListView(ObjectListView):
     action_buttons = ('export',)
 
 
-class ServiceView(PermissionRequiredMixin, View):
-    permission_required = 'ipam.view_service'
+class ServiceView(ObjectView):
+    queryset = Service.objects.all()
 
     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', {
             'service': service,

+ 5 - 4
netbox/secrets/views.py

@@ -9,7 +9,8 @@ from django.urls import reverse
 from django.views.generic import View
 
 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 .decorators import userkey_required
@@ -66,12 +67,12 @@ class SecretListView(ObjectListView):
     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):
 
-        secret = get_object_or_404(Secret, pk=pk)
+        secret = get_object_or_404(self.queryset, pk=pk)
 
         return render(request, 'secrets/secret.html', {
             '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.shortcuts import get_object_or_404, render
-from django.views.generic import View
 
 from circuits.models import Circuit
 from dcim.models import Site, Rack, Device, RackReservation
 from ipam.models import IPAddress, Prefix, VLAN, VRF
 from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
+    BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 from virtualization.models import VirtualMachine, Cluster
 from . import filters, forms, tables
@@ -59,12 +57,12 @@ class TenantListView(ObjectListView):
     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):
 
-        tenant = get_object_or_404(Tenant, slug=slug)
+        tenant = get_object_or_404(self.queryset, slug=slug)
         stats = {
             'site_count': Site.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
 #
 
+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):
     """
     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 ipam.models import Service
 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 .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -85,12 +85,12 @@ class ClusterListView(ObjectListView):
     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):
 
-        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(
             'site', 'rack', 'tenant', 'device_type__manufacturer'
         )
@@ -233,12 +233,12 @@ class VirtualMachineListView(ObjectListView):
     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):
 
-        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)
         services = Service.objects.filter(virtual_machine=virtualmachine)