Bläddra i källkod

Enforce view permissions for UI views

Jeremy Stretch 6 år sedan
förälder
incheckning
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 django.urls import reverse
 
 
 from circuits.models import Circuit, CircuitType, Provider
 from circuits.models import Circuit, CircuitType, Provider
+from utilities.testing import create_test_user
 
 
 
 
 class ProviderTestCase(TestCase):
 class ProviderTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['circuits.view_provider'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         Provider.objects.bulk_create([
         Provider.objects.bulk_create([
             Provider(name='Provider 1', slug='provider-1', asn=65001),
             Provider(name='Provider 1', slug='provider-1', asn=65001),
@@ -38,8 +40,9 @@ class ProviderTestCase(TestCase):
 class CircuitTypeTestCase(TestCase):
 class CircuitTypeTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['circuits.view_circuittype'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         CircuitType.objects.bulk_create([
         CircuitType.objects.bulk_create([
             CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
             CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
@@ -58,8 +61,9 @@ class CircuitTypeTestCase(TestCase):
 class CircuitTestCase(TestCase):
 class CircuitTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['circuits.view_circuit'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
         provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
         provider.save()
         provider.save()
@@ -84,8 +88,8 @@ class CircuitTestCase(TestCase):
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         self.assertEqual(response.status_code, 200)
         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)
         self.assertEqual(response.status_code, 200)

+ 10 - 5
netbox/circuits/views.py

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

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

@@ -1,6 +1,5 @@
 import urllib.parse
 import urllib.parse
 
 
-from django.contrib.auth import get_user_model
 from django.test import Client, TestCase
 from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
@@ -9,13 +8,15 @@ from dcim.models import (
     Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
     Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
     RackReservation, RackRole, Site, Region, VirtualChassis,
     RackReservation, RackRole, Site, Region, VirtualChassis,
 )
 )
+from utilities.testing import create_test_user
 
 
 
 
 class RegionTestCase(TestCase):
 class RegionTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_region'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         # Create three Regions
         # Create three Regions
         for i in range(1, 4):
         for i in range(1, 4):
@@ -32,8 +33,9 @@ class RegionTestCase(TestCase):
 class SiteTestCase(TestCase):
 class SiteTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_site'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         region = Region(name='Region 1', slug='region-1')
         region = Region(name='Region 1', slug='region-1')
         region.save()
         region.save()
@@ -64,8 +66,9 @@ class SiteTestCase(TestCase):
 class RackGroupTestCase(TestCase):
 class RackGroupTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rackgroup'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -84,11 +87,12 @@ class RackGroupTestCase(TestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
-class RackTypeTestCase(TestCase):
+class RackRoleTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rackrole'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         RackRole.objects.bulk_create([
         RackRole.objects.bulk_create([
             RackRole(name='Rack Role 1', slug='rack-role-1'),
             RackRole(name='Rack Role 1', slug='rack-role-1'),
@@ -107,12 +111,9 @@ class RackTypeTestCase(TestCase):
 class RackReservationTestCase(TestCase):
 class RackReservationTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rackreservation'])
         self.client = Client()
         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 = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -137,8 +138,9 @@ class RackReservationTestCase(TestCase):
 class RackTestCase(TestCase):
 class RackTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_rack'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -169,8 +171,9 @@ class RackTestCase(TestCase):
 class ManufacturerTypeTestCase(TestCase):
 class ManufacturerTypeTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_manufacturer'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         Manufacturer.objects.bulk_create([
         Manufacturer.objects.bulk_create([
             Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
             Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
@@ -189,8 +192,9 @@ class ManufacturerTypeTestCase(TestCase):
 class DeviceTypeTestCase(TestCase):
 class DeviceTypeTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_devicetype'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer.save()
         manufacturer.save()
@@ -221,8 +225,9 @@ class DeviceTypeTestCase(TestCase):
 class DeviceRoleTestCase(TestCase):
 class DeviceRoleTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_devicerole'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         DeviceRole.objects.bulk_create([
         DeviceRole.objects.bulk_create([
             DeviceRole(name='Device Role 1', slug='device-role-1'),
             DeviceRole(name='Device Role 1', slug='device-role-1'),
@@ -241,8 +246,9 @@ class DeviceRoleTestCase(TestCase):
 class PlatformTestCase(TestCase):
 class PlatformTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_platform'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         Platform.objects.bulk_create([
         Platform.objects.bulk_create([
             Platform(name='Platform 1', slug='platform-1'),
             Platform(name='Platform 1', slug='platform-1'),
@@ -261,8 +267,9 @@ class PlatformTestCase(TestCase):
 class DeviceTestCase(TestCase):
 class DeviceTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_device'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -303,8 +310,9 @@ class DeviceTestCase(TestCase):
 class InventoryItemTestCase(TestCase):
 class InventoryItemTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_inventoryitem'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -337,18 +345,13 @@ class InventoryItemTestCase(TestCase):
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         self.assertEqual(response.status_code, 200)
         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):
 class CableTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_cable'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -401,11 +404,12 @@ class CableTestCase(TestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
-class VirtualMachineTestCase(TestCase):
+class VirtualChassisTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['dcim.view_virtualchassis'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site.objects.create(name='Site 1', slug='site-1')
         site = Site.objects.create(name='Site 1', slug='site-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
@@ -450,9 +454,3 @@ class VirtualMachineTestCase(TestCase):
 
 
         response = self.client.get(url)
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
         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
 # Regions
 #
 #
 
 
-class RegionListView(ObjectListView):
+class RegionListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_region'
     queryset = Region.objects.add_related_count(
     queryset = Region.objects.add_related_count(
         Region.objects.all(),
         Region.objects.all(),
         Site,
         Site,
@@ -182,7 +183,8 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Sites
 # Sites
 #
 #
 
 
-class SiteListView(ObjectListView):
+class SiteListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_site'
     queryset = Site.objects.select_related('region', 'tenant')
     queryset = Site.objects.select_related('region', 'tenant')
     filter = filters.SiteFilter
     filter = filters.SiteFilter
     filter_form = forms.SiteFilterForm
     filter_form = forms.SiteFilterForm
@@ -190,7 +192,8 @@ class SiteListView(ObjectListView):
     template_name = 'dcim/site_list.html'
     template_name = 'dcim/site_list.html'
 
 
 
 
-class SiteView(View):
+class SiteView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_site'
 
 
     def get(self, request, slug):
     def get(self, request, slug):
 
 
@@ -254,7 +257,8 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
 # Rack groups
 # 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'))
     queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
     filter = filters.RackGroupFilter
     filter = filters.RackGroupFilter
     filter_form = forms.RackGroupFilterForm
     filter_form = forms.RackGroupFilterForm
@@ -292,7 +296,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Rack roles
 # Rack roles
 #
 #
 
 
-class RackRoleListView(ObjectListView):
+class RackRoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rackrole'
     queryset = RackRole.objects.annotate(rack_count=Count('racks'))
     queryset = RackRole.objects.annotate(rack_count=Count('racks'))
     table = tables.RackRoleTable
     table = tables.RackRoleTable
     template_name = 'dcim/rackrole_list.html'
     template_name = 'dcim/rackrole_list.html'
@@ -327,7 +332,8 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Racks
 # Racks
 #
 #
 
 
-class RackListView(ObjectListView):
+class RackListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rack'
     queryset = Rack.objects.select_related(
     queryset = Rack.objects.select_related(
         'site', 'group', 'tenant', 'role'
         'site', 'group', 'tenant', 'role'
     ).prefetch_related(
     ).prefetch_related(
@@ -341,10 +347,11 @@ class RackListView(ObjectListView):
     template_name = 'dcim/rack_list.html'
     template_name = 'dcim/rack_list.html'
 
 
 
 
-class RackElevationListView(View):
+class RackElevationListView(PermissionRequiredMixin, View):
     """
     """
     Display a set of rack elevations side-by-side.
     Display a set of rack elevations side-by-side.
     """
     """
+    permission_required = 'dcim.view_rack'
 
 
     def get(self, request):
     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):
     def get(self, request, pk):
 
 
@@ -454,7 +462,8 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Rack reservations
 # Rack reservations
 #
 #
 
 
-class RackReservationListView(ObjectListView):
+class RackReservationListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rackreservation'
     queryset = RackReservation.objects.all()
     queryset = RackReservation.objects.all()
     filter = filters.RackReservationFilter
     filter = filters.RackReservationFilter
     filter_form = forms.RackReservationFilterForm
     filter_form = forms.RackReservationFilterForm
@@ -510,7 +519,8 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Manufacturers
 # Manufacturers
 #
 #
 
 
-class ManufacturerListView(ObjectListView):
+class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_manufacturer'
     queryset = Manufacturer.objects.annotate(
     queryset = Manufacturer.objects.annotate(
         devicetype_count=Count('device_types', distinct=True),
         devicetype_count=Count('device_types', distinct=True),
         platform_count=Count('platforms', distinct=True),
         platform_count=Count('platforms', distinct=True),
@@ -548,7 +558,8 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Device types
 # 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'))
     queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
     filter = filters.DeviceTypeFilter
     filter = filters.DeviceTypeFilter
     filter_form = forms.DeviceTypeFilterForm
     filter_form = forms.DeviceTypeFilterForm
@@ -556,7 +567,8 @@ class DeviceTypeListView(ObjectListView):
     template_name = 'dcim/devicetype_list.html'
     template_name = 'dcim/devicetype_list.html'
 
 
 
 
-class DeviceTypeView(View):
+class DeviceTypeView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_devicetype'
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -812,7 +824,8 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Device roles
 # Device roles
 #
 #
 
 
-class DeviceRoleListView(ObjectListView):
+class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_devicerole'
     queryset = DeviceRole.objects.all()
     queryset = DeviceRole.objects.all()
     table = tables.DeviceRoleTable
     table = tables.DeviceRoleTable
     template_name = 'dcim/devicerole_list.html'
     template_name = 'dcim/devicerole_list.html'
@@ -847,7 +860,8 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Platforms
 # Platforms
 #
 #
 
 
-class PlatformListView(ObjectListView):
+class PlatformListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_platform'
     queryset = Platform.objects.all()
     queryset = Platform.objects.all()
     table = tables.PlatformTable
     table = tables.PlatformTable
     template_name = 'dcim/platform_list.html'
     template_name = 'dcim/platform_list.html'
@@ -882,7 +896,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Devices
 # Devices
 #
 #
 
 
-class DeviceListView(ObjectListView):
+class DeviceListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_device'
     queryset = Device.objects.select_related(
     queryset = Device.objects.select_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6'
         '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'
     template_name = 'dcim/device_list.html'
 
 
 
 
-class DeviceView(View):
+class DeviceView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_device'
 
 
     def get(self, request, pk):
     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):
     def get(self, request, pk):
 
 
@@ -993,7 +1010,7 @@ class DeviceInventoryView(View):
 
 
 
 
 class DeviceStatusView(PermissionRequiredMixin, View):
 class DeviceStatusView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.napalm_read'
+    permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -1006,7 +1023,7 @@ class DeviceStatusView(PermissionRequiredMixin, View):
 
 
 
 
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.napalm_read'
+    permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -1023,7 +1040,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 
 
 
 
 class DeviceConfigView(PermissionRequiredMixin, View):
 class DeviceConfigView(PermissionRequiredMixin, View):
-    permission_required = 'dcim.napalm_read'
+    permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
 
     def get(self, request, pk):
     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
     object_class = Device
     base_template = 'dcim/device.html'
     base_template = 'dcim/device.html'
 
 
@@ -1258,7 +1276,8 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Interfaces
 # Interfaces
 #
 #
 
 
-class InterfaceView(View):
+class InterfaceView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_interface'
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -1639,7 +1658,8 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
 # Cables
 # Cables
 #
 #
 
 
-class CableListView(ObjectListView):
+class CableListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_cable'
     queryset = Cable.objects.prefetch_related(
     queryset = Cable.objects.prefetch_related(
         'termination_a', 'termination_b'
         'termination_a', 'termination_b'
     )
     )
@@ -1649,7 +1669,8 @@ class CableListView(ObjectListView):
     template_name = 'dcim/cable_list.html'
     template_name = 'dcim/cable_list.html'
 
 
 
 
-class CableView(View):
+class CableView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_cable'
 
 
     def get(self, request, pk):
     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.
     Trace a cable path beginning from the given termination.
     """
     """
+    permission_required = 'dcim.view_cable'
 
 
     def get(self, request, model, pk):
     def get(self, request, model, pk):
 
 
@@ -1792,7 +1814,8 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Connections
 # Connections
 #
 #
 
 
-class ConsoleConnectionsListView(ObjectListView):
+class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = ('dcim.view_consoleport', 'dcim.view_consoleserverport')
     queryset = ConsolePort.objects.select_related(
     queryset = ConsolePort.objects.select_related(
         'device', 'connected_endpoint__device'
         'device', 'connected_endpoint__device'
     ).filter(
     ).filter(
@@ -1822,7 +1845,8 @@ class ConsoleConnectionsListView(ObjectListView):
         return csv_data
         return csv_data
 
 
 
 
-class PowerConnectionsListView(ObjectListView):
+class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = ('dcim.view_powerport', 'dcim.view_poweroutlet')
     queryset = PowerPort.objects.select_related(
     queryset = PowerPort.objects.select_related(
         'device', '_connected_poweroutlet__device'
         'device', '_connected_poweroutlet__device'
     ).filter(
     ).filter(
@@ -1852,7 +1876,8 @@ class PowerConnectionsListView(ObjectListView):
         return csv_data
         return csv_data
 
 
 
 
-class InterfaceConnectionsListView(ObjectListView):
+class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.interface'
     queryset = Interface.objects.select_related(
     queryset = Interface.objects.select_related(
         'device', 'cable', '_connected_interface__device'
         'device', 'cable', '_connected_interface__device'
     ).filter(
     ).filter(
@@ -1894,7 +1919,8 @@ class InterfaceConnectionsListView(ObjectListView):
 # Inventory items
 # Inventory items
 #
 #
 
 
-class InventoryItemListView(ObjectListView):
+class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_inventoryitem'
     queryset = InventoryItem.objects.select_related('device', 'manufacturer')
     queryset = InventoryItem.objects.select_related('device', 'manufacturer')
     filter = filters.InventoryItemFilter
     filter = filters.InventoryItemFilter
     filter_form = forms.InventoryItemFilterForm
     filter_form = forms.InventoryItemFilterForm
@@ -1949,7 +1975,8 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Virtual chassis
 # 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'))
     queryset = VirtualChassis.objects.select_related('master').annotate(member_count=Count('members'))
     table = tables.VirtualChassisTable
     table = tables.VirtualChassisTable
     filter = filters.VirtualChassisFilter
     filter = filters.VirtualChassisFilter
@@ -2184,7 +2211,8 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
 # Power panels
 # Power panels
 #
 #
 
 
-class PowerPanelListView(ObjectListView):
+class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_powerpanel'
     queryset = PowerPanel.objects.select_related(
     queryset = PowerPanel.objects.select_related(
         'site', 'rack_group'
         'site', 'rack_group'
     ).annotate(
     ).annotate(
@@ -2196,7 +2224,8 @@ class PowerPanelListView(ObjectListView):
     template_name = 'dcim/powerpanel_list.html'
     template_name = 'dcim/powerpanel_list.html'
 
 
 
 
-class PowerPanelView(View):
+class PowerPanelView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_powerpanel'
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -2253,7 +2282,8 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Power feeds
 # Power feeds
 #
 #
 
 
-class PowerFeedListView(ObjectListView):
+class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_powerfeed'
     queryset = PowerFeed.objects.select_related(
     queryset = PowerFeed.objects.select_related(
         'power_panel', 'rack'
         'power_panel', 'rack'
     )
     )
@@ -2263,7 +2293,8 @@ class PowerFeedListView(ObjectListView):
     template_name = 'dcim/powerfeed_list.html'
     template_name = 'dcim/powerfeed_list.html'
 
 
 
 
-class PowerFeedView(View):
+class PowerFeedView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.view_powerfeed'
 
 
     def get(self, request, pk):
     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 dcim.models import Site
 from extras.models import ConfigContext, ObjectChange, Tag
 from extras.models import ConfigContext, ObjectChange, Tag
+from utilities.testing import create_test_user
 
 
 
 
 class TagTestCase(TestCase):
 class TagTestCase(TestCase):
@@ -35,8 +36,9 @@ class TagTestCase(TestCase):
 class ConfigContextTestCase(TestCase):
 class ConfigContextTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['extras.view_configcontext'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -70,11 +72,9 @@ class ConfigContextTestCase(TestCase):
 class ObjectChangeTestCase(TestCase):
 class ObjectChangeTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['extras.view_objectchange'])
         self.client = Client()
         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 = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()

+ 12 - 6
netbox/extras/views.py

@@ -96,7 +96,8 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Config contexts
 # Config contexts
 #
 #
 
 
-class ConfigContextListView(ObjectListView):
+class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'extras.view_configcontext'
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     filter = filters.ConfigContextFilter
     filter = filters.ConfigContextFilter
     filter_form = ConfigContextFilterForm
     filter_form = ConfigContextFilterForm
@@ -104,7 +105,8 @@ class ConfigContextListView(ObjectListView):
     template_name = 'extras/configcontext_list.html'
     template_name = 'extras/configcontext_list.html'
 
 
 
 
-class ConfigContextView(View):
+class ConfigContextView(PermissionRequiredMixin, View):
+    permission_required = 'extras.view_configcontext'
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -173,7 +175,8 @@ class ObjectConfigContextView(View):
 # Change logging
 # Change logging
 #
 #
 
 
-class ObjectChangeListView(ObjectListView):
+class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'extras.view_objectchange'
     queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
     queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
     filter = filters.ObjectChangeFilter
     filter = filters.ObjectChangeFilter
     filter_form = ObjectChangeFilterForm
     filter_form = ObjectChangeFilterForm
@@ -181,7 +184,8 @@ class ObjectChangeListView(ObjectListView):
     template_name = 'extras/objectchange_list.html'
     template_name = 'extras/objectchange_list.html'
 
 
 
 
-class ObjectChangeView(View):
+class ObjectChangeView(PermissionRequiredMixin, View):
+    permission_required = 'extras.view_objectchange'
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
@@ -272,10 +276,11 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 # Reports
 # Reports
 #
 #
 
 
-class ReportListView(View):
+class ReportListView(PermissionRequiredMixin, View):
     """
     """
     Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each.
     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):
     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).
     Display a single Report and its associated ReportResult (if any).
     """
     """
+    permission_required = 'extras.view_reportresult'
 
 
     def get(self, request, name):
     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 dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from ipam.constants import IP_PROTOCOL_TCP
 from ipam.constants import IP_PROTOCOL_TCP
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
+from utilities.testing import create_test_user
 
 
 
 
 class VRFTestCase(TestCase):
 class VRFTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_vrf'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         VRF.objects.bulk_create([
         VRF.objects.bulk_create([
             VRF(name='VRF 1', rd='65000:1'),
             VRF(name='VRF 1', rd='65000:1'),
@@ -41,8 +43,9 @@ class VRFTestCase(TestCase):
 class RIRTestCase(TestCase):
 class RIRTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_rir'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         RIR.objects.bulk_create([
         RIR.objects.bulk_create([
             RIR(name='RIR 1', slug='rir-1'),
             RIR(name='RIR 1', slug='rir-1'),
@@ -57,18 +60,13 @@ class RIRTestCase(TestCase):
         response = self.client.get(url)
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
         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):
 class AggregateTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_aggregate'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         rir = RIR(name='RIR 1', slug='rir-1')
         rir = RIR(name='RIR 1', slug='rir-1')
         rir.save()
         rir.save()
@@ -99,8 +97,9 @@ class AggregateTestCase(TestCase):
 class RoleTestCase(TestCase):
 class RoleTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_role'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         Role.objects.bulk_create([
         Role.objects.bulk_create([
             Role(name='Role 1', slug='role-1'),
             Role(name='Role 1', slug='role-1'),
@@ -119,8 +118,9 @@ class RoleTestCase(TestCase):
 class PrefixTestCase(TestCase):
 class PrefixTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_prefix'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -151,8 +151,9 @@ class PrefixTestCase(TestCase):
 class IPAddressTestCase(TestCase):
 class IPAddressTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_ipaddress'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         vrf = VRF(name='VRF 1', rd='65000:1')
         vrf = VRF(name='VRF 1', rd='65000:1')
         vrf.save()
         vrf.save()
@@ -183,8 +184,9 @@ class IPAddressTestCase(TestCase):
 class VLANGroupTestCase(TestCase):
 class VLANGroupTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_vlangroup'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -209,8 +211,9 @@ class VLANGroupTestCase(TestCase):
 class VLANTestCase(TestCase):
 class VLANTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_vlan'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
         vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
         vlangroup.save()
         vlangroup.save()
@@ -241,8 +244,9 @@ class VLANTestCase(TestCase):
 class ServiceTestCase(TestCase):
 class ServiceTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['ipam.view_service'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()

+ 39 - 19
netbox/ipam/views.py

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

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

@@ -1,25 +1,19 @@
 import urllib.parse
 import urllib.parse
 
 
-from django.contrib.auth import get_user_model
 from django.test import Client, TestCase
 from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole
 from secrets.models import Secret, SecretRole
+from utilities.testing import create_test_user
 
 
 
 
 class SecretRoleTestCase(TestCase):
 class SecretRoleTestCase(TestCase):
 
 
     def setUp(self):
     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 = Client()
-        self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD)
+        self.client.force_login(user)
 
 
         SecretRole.objects.bulk_create([
         SecretRole.objects.bulk_create([
             SecretRole(name='Secret Role 1', slug='secret-role-1'),
             SecretRole(name='Secret Role 1', slug='secret-role-1'),
@@ -29,7 +23,7 @@ class SecretRoleTestCase(TestCase):
 
 
     def test_secretrole_list(self):
     def test_secretrole_list(self):
 
 
-        url = reverse('secrets:secret_list')
+        url = reverse('secrets:secretrole_list')
 
 
         response = self.client.get(url, follow=True)
         response = self.client.get(url, follow=True)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -38,8 +32,9 @@ class SecretRoleTestCase(TestCase):
 class SecretTestCase(TestCase):
 class SecretTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['secrets.view_secret'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -75,7 +70,7 @@ class SecretTestCase(TestCase):
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-    def test_configcontext(self):
+    def test_secret(self):
 
 
         secret = Secret.objects.first()
         secret = Secret.objects.first()
         response = self.client.get(secret.get_absolute_url(), follow=True)
         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
 # Secret roles
 #
 #
 
 
-class SecretRoleListView(ObjectListView):
+class SecretRoleListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'secrets.view_secretrole'
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
     template_name = 'secrets/secretrole_list.html'
     template_name = 'secrets/secretrole_list.html'
@@ -67,8 +68,8 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Secrets
 # 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')
     queryset = Secret.objects.select_related('role', 'device')
     filter = filters.SecretFilter
     filter = filters.SecretFilter
     filter_form = forms.SecretFilterForm
     filter_form = forms.SecretFilterForm
@@ -76,8 +77,8 @@ class SecretListView(ObjectListView):
     template_name = 'secrets/secret_list.html'
     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):
     def get(self, request, pk):
 
 
@@ -198,7 +199,7 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 
 
 
 
 class SecretBulkImportView(BulkImportView):
 class SecretBulkImportView(BulkImportView):
-    permission_required = 'ipam.add_vlan'
+    permission_required = 'secrets.add_secret'
     model_form = forms.SecretCSVForm
     model_form = forms.SecretCSVForm
     table = tables.SecretTable
     table = tables.SecretTable
     template_name = 'secrets/secret_import.html'
     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 django.urls import reverse
 
 
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
+from utilities.testing import create_test_user
 
 
 
 
 class TenantGroupTestCase(TestCase):
 class TenantGroupTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['tenancy.view_tenantgroup'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         TenantGroup.objects.bulk_create([
         TenantGroup.objects.bulk_create([
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
@@ -29,8 +31,9 @@ class TenantGroupTestCase(TestCase):
 class TenantTestCase(TestCase):
 class TenantTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['tenancy.view_tenant'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1')
         tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1')
         tenantgroup.save()
         tenantgroup.save()

+ 6 - 3
netbox/tenancy/views.py

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

+ 4 - 10
netbox/users/views.py

@@ -1,6 +1,5 @@
 from django.contrib import messages
 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 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.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
 from django.http import HttpResponseForbidden, HttpResponseRedirect
 from django.http import HttpResponseForbidden, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
@@ -74,8 +73,7 @@ class LogoutView(View):
 # User profiles
 # User profiles
 #
 #
 
 
-@method_decorator(login_required, name='dispatch')
-class ProfileView(View):
+class ProfileView(LoginRequiredMixin, View):
     template_name = 'users/profile.html'
     template_name = 'users/profile.html'
 
 
     def get(self, request):
     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'
     template_name = 'users/change_password.html'
 
 
     def get(self, request):
     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'
     template_name = 'users/userkey.html'
 
 
     def get(self, request):
     def get(self, request):
@@ -127,10 +123,9 @@ class UserKeyView(View):
         })
         })
 
 
 
 
-class UserKeyEditView(View):
+class UserKeyEditView(LoginRequiredMixin, View):
     template_name = 'users/userkey_edit.html'
     template_name = 'users/userkey_edit.html'
 
 
-    @method_decorator(login_required)
     def dispatch(self, request, *args, **kwargs):
     def dispatch(self, request, *args, **kwargs):
         try:
         try:
             self.userkey = UserKey.objects.get(user=request.user)
             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):
 class SessionKeyDeleteView(LoginRequiredMixin, View):
 
 
     def get(self, request):
     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 rest_framework.test import APITestCase as _APITestCase
 
 
 from users.models import Token
 from users.models import Token
@@ -22,3 +22,16 @@ class APITestCase(_APITestCase):
         self.assertEqual(response.status_code, expected_status, err_message.format(
         self.assertEqual(response.status_code, expected_status, err_message.format(
             expected_status, response.status_code, response.data
             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.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
+from utilities.testing import create_test_user
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 
 
 class ClusterGroupTestCase(TestCase):
 class ClusterGroupTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_clustergroup'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         ClusterGroup.objects.bulk_create([
         ClusterGroup.objects.bulk_create([
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
@@ -29,8 +31,9 @@ class ClusterGroupTestCase(TestCase):
 class ClusterTypeTestCase(TestCase):
 class ClusterTypeTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_clustertype'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         ClusterType.objects.bulk_create([
         ClusterType.objects.bulk_create([
             ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
             ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
@@ -49,8 +52,9 @@ class ClusterTypeTestCase(TestCase):
 class ClusterTestCase(TestCase):
 class ClusterTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_cluster'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
         clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
         clustergroup.save()
         clustergroup.save()
@@ -85,8 +89,9 @@ class ClusterTestCase(TestCase):
 class VirtualMachineTestCase(TestCase):
 class VirtualMachineTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
-
+        user = create_test_user(permissions=['virtualization.view_virtualmachine'])
         self.client = Client()
         self.client = Client()
+        self.client.force_login(user)
 
 
         clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
         clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
         clustertype.save()
         clustertype.save()

+ 14 - 7
netbox/virtualization/views.py

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