From e710ccb0e66e7027ffa4174d86c845f086fc9c24 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 11 Apr 2019 17:27:38 -0400 Subject: [PATCH] Enforce view permissions for UI views --- netbox/circuits/tests/test_views.py | 16 ++-- netbox/circuits/views.py | 15 ++-- netbox/dcim/tests/test_views.py | 64 +++++++-------- netbox/dcim/views.py | 99 +++++++++++++++-------- netbox/extras/tests/test_views.py | 10 +-- netbox/extras/views.py | 18 +++-- netbox/ipam/tests/test_views.py | 34 ++++---- netbox/ipam/views.py | 58 ++++++++----- netbox/secrets/tests/test_views.py | 19 ++--- netbox/secrets/views.py | 13 +-- netbox/tenancy/tests/test_views.py | 7 +- netbox/tenancy/views.py | 9 ++- netbox/users/views.py | 14 +--- netbox/utilities/testing.py | 15 +++- netbox/virtualization/tests/test_views.py | 13 ++- netbox/virtualization/views.py | 21 +++-- 16 files changed, 257 insertions(+), 168 deletions(-) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 65ae6d7db..cb0ea0a32 100644 --- a/netbox/circuits/tests/test_views.py +++ b/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) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 661f78e8e..24b4259f4 100644 --- a/netbox/circuits/views.py +++ b/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): diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 79f38a5c9..088806597 100644 --- a/netbox/dcim/tests/test_views.py +++ b/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) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b1403745d..c82f795b5 100644 --- a/netbox/dcim/views.py +++ b/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): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 4c91fbb50..8b13c323d 100644 --- a/netbox/extras/tests/test_views.py +++ b/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() diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 517c3e5b4..21ea4f1c9 100644 --- a/netbox/extras/views.py +++ b/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): diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 20c16df9b..e14a257d6 100644 --- a/netbox/ipam/tests/test_views.py +++ b/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() diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 2f76089a2..d80646bb0 100644 --- a/netbox/ipam/views.py +++ b/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): diff --git a/netbox/secrets/tests/test_views.py b/netbox/secrets/tests/test_views.py index f9985db28..5ba5cdcf6 100644 --- a/netbox/secrets/tests/test_views.py +++ b/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) diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 99b725528..74772beb0 100644 --- a/netbox/secrets/views.py +++ b/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' diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index a6ca8f358..877b70145 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/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() diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 97334c9f0..5d43309de 100644 --- a/netbox/tenancy/views.py +++ b/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): diff --git a/netbox/users/views.py b/netbox/users/views.py index 6ec984936..d71a5a502 100644 --- a/netbox/users/views.py +++ b/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): diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing.py index 86fa8c836..0534a340b 100644 --- a/netbox/utilities/testing.py +++ b/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 diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 1b86e2015..b967eeefc 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/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() diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index aa8a585a9..712c7acb5 100644 --- a/netbox/virtualization/views.py +++ b/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'