Sfoglia il codice sorgente

Enforce view permissions for UI views

Jeremy Stretch 6 anni fa
parent
commit
e710ccb0e6

+ 10 - 6
netbox/circuits/tests/test_views.py

@@ -4,13 +4,15 @@ from django.test import Client, TestCase
 from django.urls import reverse
 
 from circuits.models import Circuit, CircuitType, Provider
+from utilities.testing import create_test_user
 
 
 class ProviderTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['circuits.view_provider'])
         self.client = Client()
+        self.client.force_login(user)
 
         Provider.objects.bulk_create([
             Provider(name='Provider 1', slug='provider-1', asn=65001),
@@ -38,8 +40,9 @@ class ProviderTestCase(TestCase):
 class CircuitTypeTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['circuits.view_circuittype'])
         self.client = Client()
+        self.client.force_login(user)
 
         CircuitType.objects.bulk_create([
             CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
@@ -58,8 +61,9 @@ class CircuitTypeTestCase(TestCase):
 class CircuitTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['circuits.view_circuit'])
         self.client = Client()
+        self.client.force_login(user)
 
         provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
         provider.save()
@@ -84,8 +88,8 @@ class CircuitTestCase(TestCase):
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         self.assertEqual(response.status_code, 200)
 
-    def test_provider(self):
+    def test_circuit(self):
 
-        provider = Provider.objects.first()
-        response = self.client.get(provider.get_absolute_url())
+        circuit = Circuit.objects.first()
+        response = self.client.get(circuit.get_absolute_url())
         self.assertEqual(response.status_code, 200)

+ 10 - 5
netbox/circuits/views.py

@@ -20,7 +20,8 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 # Providers
 #
 
-class ProviderListView(ObjectListView):
+class ProviderListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'circuits.view_provider'
     queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
     filter = filters.ProviderFilter
     filter_form = forms.ProviderFilterForm
@@ -28,7 +29,8 @@ class ProviderListView(ObjectListView):
     template_name = 'circuits/provider_list.html'
 
 
-class ProviderView(View):
+class ProviderView(PermissionRequiredMixin, View):
+    permission_required = 'circuits.view_provider'
 
     def get(self, request, slug):
 
@@ -93,7 +95,8 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Circuit Types
 #
 
-class CircuitTypeListView(ObjectListView):
+class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'circuits.view_circuittype'
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
     table = tables.CircuitTypeTable
     template_name = 'circuits/circuittype_list.html'
@@ -128,7 +131,8 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Circuits
 #
 
-class CircuitListView(ObjectListView):
+class CircuitListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'circuits.view_circuit'
     queryset = Circuit.objects.select_related(
         'provider', 'type', 'tenant'
     ).prefetch_related(
@@ -140,7 +144,8 @@ class CircuitListView(ObjectListView):
     template_name = 'circuits/circuit_list.html'
 
 
-class CircuitView(View):
+class CircuitView(PermissionRequiredMixin, View):
+    permission_required = 'circuits.view_circuit'
 
     def get(self, request, pk):
 

+ 31 - 33
netbox/dcim/tests/test_views.py

@@ -1,6 +1,5 @@
 import urllib.parse
 
-from django.contrib.auth import get_user_model
 from django.test import Client, TestCase
 from django.urls import reverse
 
@@ -9,13 +8,15 @@ from dcim.models import (
     Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
     RackReservation, RackRole, Site, Region, VirtualChassis,
 )
+from utilities.testing import create_test_user
 
 
 class RegionTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_region'])
         self.client = Client()
+        self.client.force_login(user)
 
         # Create three Regions
         for i in range(1, 4):
@@ -32,8 +33,9 @@ class RegionTestCase(TestCase):
 class SiteTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_site'])
         self.client = Client()
+        self.client.force_login(user)
 
         region = Region(name='Region 1', slug='region-1')
         region.save()
@@ -64,8 +66,9 @@ class SiteTestCase(TestCase):
 class RackGroupTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rackgroup'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -84,11 +87,12 @@ class RackGroupTestCase(TestCase):
         self.assertEqual(response.status_code, 200)
 
 
-class RackTypeTestCase(TestCase):
+class RackRoleTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rackrole'])
         self.client = Client()
+        self.client.force_login(user)
 
         RackRole.objects.bulk_create([
             RackRole(name='Rack Role 1', slug='rack-role-1'),
@@ -107,12 +111,9 @@ class RackTypeTestCase(TestCase):
 class RackReservationTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rackreservation'])
         self.client = Client()
-
-        User = get_user_model()
-        user = User(username='testuser', email='testuser@example.com')
-        user.save()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -137,8 +138,9 @@ class RackReservationTestCase(TestCase):
 class RackTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rack'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -169,8 +171,9 @@ class RackTestCase(TestCase):
 class ManufacturerTypeTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_manufacturer'])
         self.client = Client()
+        self.client.force_login(user)
 
         Manufacturer.objects.bulk_create([
             Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
@@ -189,8 +192,9 @@ class ManufacturerTypeTestCase(TestCase):
 class DeviceTypeTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_devicetype'])
         self.client = Client()
+        self.client.force_login(user)
 
         manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer.save()
@@ -221,8 +225,9 @@ class DeviceTypeTestCase(TestCase):
 class DeviceRoleTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_devicerole'])
         self.client = Client()
+        self.client.force_login(user)
 
         DeviceRole.objects.bulk_create([
             DeviceRole(name='Device Role 1', slug='device-role-1'),
@@ -241,8 +246,9 @@ class DeviceRoleTestCase(TestCase):
 class PlatformTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_platform'])
         self.client = Client()
+        self.client.force_login(user)
 
         Platform.objects.bulk_create([
             Platform(name='Platform 1', slug='platform-1'),
@@ -261,8 +267,9 @@ class PlatformTestCase(TestCase):
 class DeviceTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_device'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -303,8 +310,9 @@ class DeviceTestCase(TestCase):
 class InventoryItemTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_inventoryitem'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -337,18 +345,13 @@ class InventoryItemTestCase(TestCase):
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         self.assertEqual(response.status_code, 200)
 
-    def test_inventoryitem(self):
-
-        inventoryitem = InventoryItem.objects.first()
-        response = self.client.get(inventoryitem.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
 
 class CableTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_cable'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -401,11 +404,12 @@ class CableTestCase(TestCase):
         self.assertEqual(response.status_code, 200)
 
 
-class VirtualMachineTestCase(TestCase):
+class VirtualChassisTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_virtualchassis'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site.objects.create(name='Site 1', slug='site-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
@@ -450,9 +454,3 @@ class VirtualMachineTestCase(TestCase):
 
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
-
-    def test_virtualchassis(self):
-
-        virtualchassis = VirtualChassis.objects.first()
-        response = self.client.get(virtualchassis.get_absolute_url())
-        self.assertEqual(response.status_code, 200)

+ 65 - 34
netbox/dcim/views.py

@@ -138,7 +138,8 @@ class BulkDisconnectView(GetReturnURLMixin, View):
 # Regions
 #
 
-class RegionListView(ObjectListView):
+class RegionListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_region'
     queryset = Region.objects.add_related_count(
         Region.objects.all(),
         Site,
@@ -182,7 +183,8 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Sites
 #
 
-class SiteListView(ObjectListView):
+class SiteListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_site'
     queryset = Site.objects.select_related('region', 'tenant')
     filter = filters.SiteFilter
     filter_form = forms.SiteFilterForm
@@ -190,7 +192,8 @@ class SiteListView(ObjectListView):
     template_name = 'dcim/site_list.html'
 
 
-class SiteView(View):
+class SiteView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_site'
 
     def get(self, request, slug):
 
@@ -254,7 +257,8 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
 # Rack groups
 #
 
-class RackGroupListView(ObjectListView):
+class RackGroupListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rackgroup'
     queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
     filter = filters.RackGroupFilter
     filter_form = forms.RackGroupFilterForm
@@ -292,7 +296,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Rack roles
 #
 
-class RackRoleListView(ObjectListView):
+class RackRoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rackrole'
     queryset = RackRole.objects.annotate(rack_count=Count('racks'))
     table = tables.RackRoleTable
     template_name = 'dcim/rackrole_list.html'
@@ -327,7 +332,8 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Racks
 #
 
-class RackListView(ObjectListView):
+class RackListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rack'
     queryset = Rack.objects.select_related(
         'site', 'group', 'tenant', 'role'
     ).prefetch_related(
@@ -341,10 +347,11 @@ class RackListView(ObjectListView):
     template_name = 'dcim/rack_list.html'
 
 
-class RackElevationListView(View):
+class RackElevationListView(PermissionRequiredMixin, View):
     """
     Display a set of rack elevations side-by-side.
     """
+    permission_required = 'dcim.view_rack'
 
     def get(self, request):
 
@@ -382,7 +389,8 @@ class RackElevationListView(View):
         })
 
 
-class RackView(View):
+class RackView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_rack'
 
     def get(self, request, pk):
 
@@ -454,7 +462,8 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Rack reservations
 #
 
-class RackReservationListView(ObjectListView):
+class RackReservationListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rackreservation'
     queryset = RackReservation.objects.all()
     filter = filters.RackReservationFilter
     filter_form = forms.RackReservationFilterForm
@@ -510,7 +519,8 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Manufacturers
 #
 
-class ManufacturerListView(ObjectListView):
+class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_manufacturer'
     queryset = Manufacturer.objects.annotate(
         devicetype_count=Count('device_types', distinct=True),
         platform_count=Count('platforms', distinct=True),
@@ -548,7 +558,8 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Device types
 #
 
-class DeviceTypeListView(ObjectListView):
+class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_devicetype'
     queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
     filter = filters.DeviceTypeFilter
     filter_form = forms.DeviceTypeFilterForm
@@ -556,7 +567,8 @@ class DeviceTypeListView(ObjectListView):
     template_name = 'dcim/devicetype_list.html'
 
 
-class DeviceTypeView(View):
+class DeviceTypeView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_devicetype'
 
     def get(self, request, pk):
 
@@ -812,7 +824,8 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Device roles
 #
 
-class DeviceRoleListView(ObjectListView):
+class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_devicerole'
     queryset = DeviceRole.objects.all()
     table = tables.DeviceRoleTable
     template_name = 'dcim/devicerole_list.html'
@@ -847,7 +860,8 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Platforms
 #
 
-class PlatformListView(ObjectListView):
+class PlatformListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_platform'
     queryset = Platform.objects.all()
     table = tables.PlatformTable
     template_name = 'dcim/platform_list.html'
@@ -882,7 +896,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Devices
 #
 
-class DeviceListView(ObjectListView):
+class DeviceListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_device'
     queryset = Device.objects.select_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6'
     )
@@ -892,7 +907,8 @@ class DeviceListView(ObjectListView):
     template_name = 'dcim/device_list.html'
 
 
-class DeviceView(View):
+class DeviceView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_device'
 
     def get(self, request, pk):
 
@@ -972,7 +988,8 @@ class DeviceView(View):
         })
 
 
-class DeviceInventoryView(View):
+class DeviceInventoryView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_device'
 
     def get(self, request, pk):
 
@@ -993,7 +1010,7 @@ class DeviceInventoryView(View):
 
 
 class DeviceStatusView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.napalm_read'
+    permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
     def get(self, request, pk):
 
@@ -1006,7 +1023,7 @@ class DeviceStatusView(PermissionRequiredMixin, View):
 
 
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.napalm_read'
+    permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
     def get(self, request, pk):
 
@@ -1023,7 +1040,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 
 
 class DeviceConfigView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.napalm_read'
+    permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
     def get(self, request, pk):
 
@@ -1035,7 +1052,8 @@ class DeviceConfigView(PermissionRequiredMixin, View):
         })
 
 
-class DeviceConfigContextView(ObjectConfigContextView):
+class DeviceConfigContextView(PermissionRequiredMixin, ObjectConfigContextView):
+    permission_required = 'dcim.view_device'
     object_class = Device
     base_template = 'dcim/device.html'
 
@@ -1258,7 +1276,8 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Interfaces
 #
 
-class InterfaceView(View):
+class InterfaceView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_interface'
 
     def get(self, request, pk):
 
@@ -1639,7 +1658,8 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
 # Cables
 #
 
-class CableListView(ObjectListView):
+class CableListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_cable'
     queryset = Cable.objects.prefetch_related(
         'termination_a', 'termination_b'
     )
@@ -1649,7 +1669,8 @@ class CableListView(ObjectListView):
     template_name = 'dcim/cable_list.html'
 
 
-class CableView(View):
+class CableView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_cable'
 
     def get(self, request, pk):
 
@@ -1660,10 +1681,11 @@ class CableView(View):
         })
 
 
-class CableTraceView(View):
+class CableTraceView(PermissionRequiredMixin, View):
     """
     Trace a cable path beginning from the given termination.
     """
+    permission_required = 'dcim.view_cable'
 
     def get(self, request, model, pk):
 
@@ -1792,7 +1814,8 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Connections
 #
 
-class ConsoleConnectionsListView(ObjectListView):
+class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = ('dcim.view_consoleport', 'dcim.view_consoleserverport')
     queryset = ConsolePort.objects.select_related(
         'device', 'connected_endpoint__device'
     ).filter(
@@ -1822,7 +1845,8 @@ class ConsoleConnectionsListView(ObjectListView):
         return csv_data
 
 
-class PowerConnectionsListView(ObjectListView):
+class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = ('dcim.view_powerport', 'dcim.view_poweroutlet')
     queryset = PowerPort.objects.select_related(
         'device', '_connected_poweroutlet__device'
     ).filter(
@@ -1852,7 +1876,8 @@ class PowerConnectionsListView(ObjectListView):
         return csv_data
 
 
-class InterfaceConnectionsListView(ObjectListView):
+class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.interface'
     queryset = Interface.objects.select_related(
         'device', 'cable', '_connected_interface__device'
     ).filter(
@@ -1894,7 +1919,8 @@ class InterfaceConnectionsListView(ObjectListView):
 # Inventory items
 #
 
-class InventoryItemListView(ObjectListView):
+class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_inventoryitem'
     queryset = InventoryItem.objects.select_related('device', 'manufacturer')
     filter = filters.InventoryItemFilter
     filter_form = forms.InventoryItemFilterForm
@@ -1949,7 +1975,8 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Virtual chassis
 #
 
-class VirtualChassisListView(ObjectListView):
+class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_virtualchassis'
     queryset = VirtualChassis.objects.select_related('master').annotate(member_count=Count('members'))
     table = tables.VirtualChassisTable
     filter = filters.VirtualChassisFilter
@@ -2184,7 +2211,8 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
 # Power panels
 #
 
-class PowerPanelListView(ObjectListView):
+class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_powerpanel'
     queryset = PowerPanel.objects.select_related(
         'site', 'rack_group'
     ).annotate(
@@ -2196,7 +2224,8 @@ class PowerPanelListView(ObjectListView):
     template_name = 'dcim/powerpanel_list.html'
 
 
-class PowerPanelView(View):
+class PowerPanelView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_powerpanel'
 
     def get(self, request, pk):
 
@@ -2253,7 +2282,8 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Power feeds
 #
 
-class PowerFeedListView(ObjectListView):
+class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_powerfeed'
     queryset = PowerFeed.objects.select_related(
         'power_panel', 'rack'
     )
@@ -2263,7 +2293,8 @@ class PowerFeedListView(ObjectListView):
     template_name = 'dcim/powerfeed_list.html'
 
 
-class PowerFeedView(View):
+class PowerFeedView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_powerfeed'
 
     def get(self, request, pk):
 

+ 5 - 5
netbox/extras/tests/test_views.py

@@ -7,6 +7,7 @@ from django.urls import reverse
 
 from dcim.models import Site
 from extras.models import ConfigContext, ObjectChange, Tag
+from utilities.testing import create_test_user
 
 
 class TagTestCase(TestCase):
@@ -35,8 +36,9 @@ class TagTestCase(TestCase):
 class ConfigContextTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['extras.view_configcontext'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -70,11 +72,9 @@ class ConfigContextTestCase(TestCase):
 class ObjectChangeTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['extras.view_objectchange'])
         self.client = Client()
-
-        user = User(username='testuser', email='testuser@example.com')
-        user.save()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()

+ 12 - 6
netbox/extras/views.py

@@ -96,7 +96,8 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Config contexts
 #
 
-class ConfigContextListView(ObjectListView):
+class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'extras.view_configcontext'
     queryset = ConfigContext.objects.all()
     filter = filters.ConfigContextFilter
     filter_form = ConfigContextFilterForm
@@ -104,7 +105,8 @@ class ConfigContextListView(ObjectListView):
     template_name = 'extras/configcontext_list.html'
 
 
-class ConfigContextView(View):
+class ConfigContextView(PermissionRequiredMixin, View):
+    permission_required = 'extras.view_configcontext'
 
     def get(self, request, pk):
 
@@ -173,7 +175,8 @@ class ObjectConfigContextView(View):
 # Change logging
 #
 
-class ObjectChangeListView(ObjectListView):
+class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'extras.view_objectchange'
     queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
     filter = filters.ObjectChangeFilter
     filter_form = ObjectChangeFilterForm
@@ -181,7 +184,8 @@ class ObjectChangeListView(ObjectListView):
     template_name = 'extras/objectchange_list.html'
 
 
-class ObjectChangeView(View):
+class ObjectChangeView(PermissionRequiredMixin, View):
+    permission_required = 'extras.view_objectchange'
 
     def get(self, request, pk):
 
@@ -272,10 +276,11 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 # Reports
 #
 
-class ReportListView(View):
+class ReportListView(PermissionRequiredMixin, View):
     """
     Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each.
     """
+    permission_required = 'extras.view_reportresult'
 
     def get(self, request):
 
@@ -295,10 +300,11 @@ class ReportListView(View):
         })
 
 
-class ReportView(View):
+class ReportView(PermissionRequiredMixin, View):
     """
     Display a single Report and its associated ReportResult (if any).
     """
+    permission_required = 'extras.view_reportresult'
 
     def get(self, request, name):
 

+ 19 - 15
netbox/ipam/tests/test_views.py

@@ -7,13 +7,15 @@ from django.urls import reverse
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from ipam.constants import IP_PROTOCOL_TCP
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
+from utilities.testing import create_test_user
 
 
 class VRFTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_vrf'])
         self.client = Client()
+        self.client.force_login(user)
 
         VRF.objects.bulk_create([
             VRF(name='VRF 1', rd='65000:1'),
@@ -41,8 +43,9 @@ class VRFTestCase(TestCase):
 class RIRTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_rir'])
         self.client = Client()
+        self.client.force_login(user)
 
         RIR.objects.bulk_create([
             RIR(name='RIR 1', slug='rir-1'),
@@ -57,18 +60,13 @@ class RIRTestCase(TestCase):
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
 
-    def test_rir(self):
-
-        rir = RIR.objects.first()
-        response = self.client.get(rir.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
 
 class AggregateTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_aggregate'])
         self.client = Client()
+        self.client.force_login(user)
 
         rir = RIR(name='RIR 1', slug='rir-1')
         rir.save()
@@ -99,8 +97,9 @@ class AggregateTestCase(TestCase):
 class RoleTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_role'])
         self.client = Client()
+        self.client.force_login(user)
 
         Role.objects.bulk_create([
             Role(name='Role 1', slug='role-1'),
@@ -119,8 +118,9 @@ class RoleTestCase(TestCase):
 class PrefixTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_prefix'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -151,8 +151,9 @@ class PrefixTestCase(TestCase):
 class IPAddressTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_ipaddress'])
         self.client = Client()
+        self.client.force_login(user)
 
         vrf = VRF(name='VRF 1', rd='65000:1')
         vrf.save()
@@ -183,8 +184,9 @@ class IPAddressTestCase(TestCase):
 class VLANGroupTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_vlangroup'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -209,8 +211,9 @@ class VLANGroupTestCase(TestCase):
 class VLANTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_vlan'])
         self.client = Client()
+        self.client.force_login(user)
 
         vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
         vlangroup.save()
@@ -241,8 +244,9 @@ class VLANTestCase(TestCase):
 class ServiceTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_service'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()

+ 39 - 19
netbox/ipam/views.py

@@ -113,7 +113,8 @@ def add_available_vlans(vlan_group, vlans):
 # VRFs
 #
 
-class VRFListView(ObjectListView):
+class VRFListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_vrf'
     queryset = VRF.objects.select_related('tenant')
     filter = filters.VRFFilter
     filter_form = forms.VRFFilterForm
@@ -121,7 +122,8 @@ class VRFListView(ObjectListView):
     template_name = 'ipam/vrf_list.html'
 
 
-class VRFView(View):
+class VRFView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_vrf'
 
     def get(self, request, pk):
 
@@ -180,7 +182,8 @@ class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # RIRs
 #
 
-class RIRListView(ObjectListView):
+class RIRListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_rir'
     queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
     filter = filters.RIRFilter
     filter_form = forms.RIRFilterForm
@@ -286,7 +289,8 @@ class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Aggregates
 #
 
-class AggregateListView(ObjectListView):
+class AggregateListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_aggregate'
     queryset = Aggregate.objects.select_related('rir').extra(select={
         'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
     })
@@ -312,7 +316,8 @@ class AggregateListView(ObjectListView):
         }
 
 
-class AggregateView(View):
+class AggregateView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_aggregate'
 
     def get(self, request, pk):
 
@@ -398,7 +403,8 @@ class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Prefix/VLAN roles
 #
 
-class RoleListView(ObjectListView):
+class RoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_role'
     queryset = Role.objects.all()
     table = tables.RoleTable
     template_name = 'ipam/role_list.html'
@@ -433,7 +439,8 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Prefixes
 #
 
-class PrefixListView(ObjectListView):
+class PrefixListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_prefix'
     queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     filter = filters.PrefixFilter
     filter_form = forms.PrefixFilterForm
@@ -446,7 +453,8 @@ class PrefixListView(ObjectListView):
         return self.queryset.annotate_depth(limit=limit)
 
 
-class PrefixView(View):
+class PrefixView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_prefix'
 
     def get(self, request, pk):
 
@@ -489,7 +497,8 @@ class PrefixView(View):
         })
 
 
-class PrefixPrefixesView(View):
+class PrefixPrefixesView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_prefix'
 
     def get(self, request, pk):
 
@@ -531,7 +540,8 @@ class PrefixPrefixesView(View):
         })
 
 
-class PrefixIPAddressesView(View):
+class PrefixIPAddressesView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_prefix'
 
     def get(self, request, pk):
 
@@ -617,7 +627,8 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # IP addresses
 #
 
-class IPAddressListView(ObjectListView):
+class IPAddressListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_ipaddress'
     queryset = IPAddress.objects.select_related(
         'vrf__tenant', 'tenant', 'nat_inside'
     ).prefetch_related(
@@ -629,7 +640,8 @@ class IPAddressListView(ObjectListView):
     template_name = 'ipam/ipaddress_list.html'
 
 
-class IPAddressView(View):
+class IPAddressView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_ipaddress'
 
     def get(self, request, pk):
 
@@ -788,7 +800,8 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # VLAN groups
 #
 
-class VLANGroupListView(ObjectListView):
+class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_vlangroup'
     queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
     filter = filters.VLANGroupFilter
     filter_form = forms.VLANGroupFilterForm
@@ -822,7 +835,9 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     default_return_url = 'ipam:vlangroup_list'
 
 
-class VLANGroupVLANsView(View):
+class VLANGroupVLANsView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_vlangroup'
+
     def get(self, request, pk):
 
         vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
@@ -861,7 +876,8 @@ class VLANGroupVLANsView(View):
 # VLANs
 #
 
-class VLANListView(ObjectListView):
+class VLANListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_vlan'
     queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
     filter = filters.VLANFilter
     filter_form = forms.VLANFilterForm
@@ -869,7 +885,8 @@ class VLANListView(ObjectListView):
     template_name = 'ipam/vlan_list.html'
 
 
-class VLANView(View):
+class VLANView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_vlan'
 
     def get(self, request, pk):
 
@@ -886,7 +903,8 @@ class VLANView(View):
         })
 
 
-class VLANMembersView(View):
+class VLANMembersView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_vlan'
 
     def get(self, request, pk):
 
@@ -954,7 +972,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Services
 #
 
-class ServiceListView(ObjectListView):
+class ServiceListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'ipam.view_service'
     queryset = Service.objects.select_related('device', 'virtual_machine')
     filter = filters.ServiceFilter
     filter_form = forms.ServiceFilterForm
@@ -962,7 +981,8 @@ class ServiceListView(ObjectListView):
     template_name = 'ipam/service_list.html'
 
 
-class ServiceView(View):
+class ServiceView(PermissionRequiredMixin, View):
+    permission_required = 'ipam.view_service'
 
     def get(self, request, pk):
 

+ 7 - 12
netbox/secrets/tests/test_views.py

@@ -1,25 +1,19 @@
 import urllib.parse
 
-from django.contrib.auth import get_user_model
 from django.test import Client, TestCase
 from django.urls import reverse
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole
+from utilities.testing import create_test_user
 
 
 class SecretRoleTestCase(TestCase):
 
     def setUp(self):
-
-        TEST_USERNAME = 'testuser'
-        TEST_PASSWORD = 'testpassword'
-
-        User = get_user_model()
-        User.objects.create(username=TEST_USERNAME, email='testuser@example.com', password=TEST_PASSWORD)
-
+        user = create_test_user(permissions=['secrets.view_secretrole'])
         self.client = Client()
-        self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD)
+        self.client.force_login(user)
 
         SecretRole.objects.bulk_create([
             SecretRole(name='Secret Role 1', slug='secret-role-1'),
@@ -29,7 +23,7 @@ class SecretRoleTestCase(TestCase):
 
     def test_secretrole_list(self):
 
-        url = reverse('secrets:secret_list')
+        url = reverse('secrets:secretrole_list')
 
         response = self.client.get(url, follow=True)
         self.assertEqual(response.status_code, 200)
@@ -38,8 +32,9 @@ class SecretRoleTestCase(TestCase):
 class SecretTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['secrets.view_secret'])
         self.client = Client()
+        self.client.force_login(user)
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -75,7 +70,7 @@ class SecretTestCase(TestCase):
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
         self.assertEqual(response.status_code, 200)
 
-    def test_configcontext(self):
+    def test_secret(self):
 
         secret = Secret.objects.first()
         response = self.client.get(secret.get_absolute_url(), follow=True)

+ 7 - 6
netbox/secrets/views.py

@@ -32,7 +32,8 @@ def get_session_key(request):
 # Secret roles
 #
 
-class SecretRoleListView(ObjectListView):
+class SecretRoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'secrets.view_secretrole'
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
     table = tables.SecretRoleTable
     template_name = 'secrets/secretrole_list.html'
@@ -67,8 +68,8 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Secrets
 #
 
-@method_decorator(login_required, name='dispatch')
-class SecretListView(ObjectListView):
+class SecretListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'secrets.view_secret'
     queryset = Secret.objects.select_related('role', 'device')
     filter = filters.SecretFilter
     filter_form = forms.SecretFilterForm
@@ -76,8 +77,8 @@ class SecretListView(ObjectListView):
     template_name = 'secrets/secret_list.html'
 
 
-@method_decorator(login_required, name='dispatch')
-class SecretView(View):
+class SecretView(PermissionRequiredMixin, View):
+    permission_required = 'secrets.view_secret'
 
     def get(self, request, pk):
 
@@ -198,7 +199,7 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 
 
 class SecretBulkImportView(BulkImportView):
-    permission_required = 'ipam.add_vlan'
+    permission_required = 'secrets.add_secret'
     model_form = forms.SecretCSVForm
     table = tables.SecretTable
     template_name = 'secrets/secret_import.html'

+ 5 - 2
netbox/tenancy/tests/test_views.py

@@ -4,13 +4,15 @@ from django.test import Client, TestCase
 from django.urls import reverse
 
 from tenancy.models import Tenant, TenantGroup
+from utilities.testing import create_test_user
 
 
 class TenantGroupTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['tenancy.view_tenantgroup'])
         self.client = Client()
+        self.client.force_login(user)
 
         TenantGroup.objects.bulk_create([
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
@@ -29,8 +31,9 @@ class TenantGroupTestCase(TestCase):
 class TenantTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['tenancy.view_tenant'])
         self.client = Client()
+        self.client.force_login(user)
 
         tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1')
         tenantgroup.save()

+ 6 - 3
netbox/tenancy/views.py

@@ -18,7 +18,8 @@ from .models import Tenant, TenantGroup
 # Tenant groups
 #
 
-class TenantGroupListView(ObjectListView):
+class TenantGroupListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'tenancy.view_tenantgroup'
     queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
     table = tables.TenantGroupTable
     template_name = 'tenancy/tenantgroup_list.html'
@@ -53,7 +54,8 @@ class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 #  Tenants
 #
 
-class TenantListView(ObjectListView):
+class TenantListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'tenancy.view_tenant'
     queryset = Tenant.objects.select_related('group')
     filter = filters.TenantFilter
     filter_form = forms.TenantFilterForm
@@ -61,7 +63,8 @@ class TenantListView(ObjectListView):
     template_name = 'tenancy/tenant_list.html'
 
 
-class TenantView(View):
+class TenantView(PermissionRequiredMixin, View):
+    permission_required = 'tenancy.view_tenant'
 
     def get(self, request, slug):
 

+ 4 - 10
netbox/users/views.py

@@ -1,6 +1,5 @@
 from django.contrib import messages
 from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
-from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
 from django.http import HttpResponseForbidden, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
@@ -74,8 +73,7 @@ class LogoutView(View):
 # User profiles
 #
 
-@method_decorator(login_required, name='dispatch')
-class ProfileView(View):
+class ProfileView(LoginRequiredMixin, View):
     template_name = 'users/profile.html'
 
     def get(self, request):
@@ -85,8 +83,7 @@ class ProfileView(View):
         })
 
 
-@method_decorator(login_required, name='dispatch')
-class ChangePasswordView(View):
+class ChangePasswordView(LoginRequiredMixin, View):
     template_name = 'users/change_password.html'
 
     def get(self, request):
@@ -111,8 +108,7 @@ class ChangePasswordView(View):
         })
 
 
-@method_decorator(login_required, name='dispatch')
-class UserKeyView(View):
+class UserKeyView(LoginRequiredMixin, View):
     template_name = 'users/userkey.html'
 
     def get(self, request):
@@ -127,10 +123,9 @@ class UserKeyView(View):
         })
 
 
-class UserKeyEditView(View):
+class UserKeyEditView(LoginRequiredMixin, View):
     template_name = 'users/userkey_edit.html'
 
-    @method_decorator(login_required)
     def dispatch(self, request, *args, **kwargs):
         try:
             self.userkey = UserKey.objects.get(user=request.user)
@@ -164,7 +159,6 @@ class UserKeyEditView(View):
         })
 
 
-@method_decorator(login_required, name='dispatch')
 class SessionKeyDeleteView(LoginRequiredMixin, View):
 
     def get(self, request):

+ 14 - 1
netbox/utilities/testing.py

@@ -1,4 +1,4 @@
-from django.contrib.auth.models import User
+from django.contrib.auth.models import Permission, User
 from rest_framework.test import APITestCase as _APITestCase
 
 from users.models import Token
@@ -22,3 +22,16 @@ class APITestCase(_APITestCase):
         self.assertEqual(response.status_code, expected_status, err_message.format(
             expected_status, response.status_code, response.data
         ))
+
+
+def create_test_user(username='testuser', permissions=list()):
+    """
+    Create a User with the given permissions.
+    """
+    user = User.objects.create_user(username=username)
+    for perm_name in permissions:
+        app, codename = perm_name.split('.')
+        perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+        user.user_permissions.add(perm)
+
+    return user

+ 9 - 4
netbox/virtualization/tests/test_views.py

@@ -3,14 +3,16 @@ import urllib.parse
 from django.test import Client, TestCase
 from django.urls import reverse
 
+from utilities.testing import create_test_user
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 class ClusterGroupTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_clustergroup'])
         self.client = Client()
+        self.client.force_login(user)
 
         ClusterGroup.objects.bulk_create([
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
@@ -29,8 +31,9 @@ class ClusterGroupTestCase(TestCase):
 class ClusterTypeTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_clustertype'])
         self.client = Client()
+        self.client.force_login(user)
 
         ClusterType.objects.bulk_create([
             ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
@@ -49,8 +52,9 @@ class ClusterTypeTestCase(TestCase):
 class ClusterTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_cluster'])
         self.client = Client()
+        self.client.force_login(user)
 
         clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
         clustergroup.save()
@@ -85,8 +89,9 @@ class ClusterTestCase(TestCase):
 class VirtualMachineTestCase(TestCase):
 
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_virtualmachine'])
         self.client = Client()
+        self.client.force_login(user)
 
         clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
         clustertype.save()

+ 14 - 7
netbox/virtualization/views.py

@@ -22,7 +22,8 @@ from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 # Cluster types
 #
 
-class ClusterTypeListView(ObjectListView):
+class ClusterTypeListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'virtualization.view_clustertype'
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
     table = tables.ClusterTypeTable
     template_name = 'virtualization/clustertype_list.html'
@@ -57,7 +58,8 @@ class ClusterTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Cluster groups
 #
 
-class ClusterGroupListView(ObjectListView):
+class ClusterGroupListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'virtualization.view_clustergroup'
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
     table = tables.ClusterGroupTable
     template_name = 'virtualization/clustergroup_list.html'
@@ -92,7 +94,8 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Clusters
 #
 
-class ClusterListView(ObjectListView):
+class ClusterListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'virtualization.view_cluster'
     queryset = Cluster.objects.select_related('type', 'group', 'site')
     table = tables.ClusterTable
     filter = filters.ClusterFilter
@@ -100,7 +103,8 @@ class ClusterListView(ObjectListView):
     template_name = 'virtualization/cluster_list.html'
 
 
-class ClusterView(View):
+class ClusterView(PermissionRequiredMixin, View):
+    permission_required = 'virtualization.view_cluster'
 
     def get(self, request, pk):
 
@@ -247,7 +251,8 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
 # Virtual machines
 #
 
-class VirtualMachineListView(ObjectListView):
+class VirtualMachineListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'virtualization.view_virtualmachine'
     queryset = VirtualMachine.objects.select_related('cluster', 'tenant', 'role', 'primary_ip4', 'primary_ip6')
     filter = filters.VirtualMachineFilter
     filter_form = forms.VirtualMachineFilterForm
@@ -255,7 +260,8 @@ class VirtualMachineListView(ObjectListView):
     template_name = 'virtualization/virtualmachine_list.html'
 
 
-class VirtualMachineView(View):
+class VirtualMachineView(PermissionRequiredMixin, View):
+    permission_required = 'virtualization.view_virtualmachine'
 
     def get(self, request, pk):
 
@@ -270,7 +276,8 @@ class VirtualMachineView(View):
         })
 
 
-class VirtualMachineConfigContextView(ObjectConfigContextView):
+class VirtualMachineConfigContextView(PermissionRequiredMixin, ObjectConfigContextView):
+    permission_required = 'virtualization.view_virtualmachine'
     object_class = VirtualMachine
     base_template = 'virtualization/virtualmachine.html'