Merge pull request #3066 from digitalocean/323-view-permissions

Closes #323
This commit is contained in:
Jeremy Stretch 2019-04-12 09:33:48 -04:00 committed by GitHub
commit 573af6a236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 676 additions and 404 deletions

View File

@ -16,8 +16,31 @@ NetBox now makes use of its own `Tag` model instead of the vanilla model which s
lives in the `extras` app and thus any permissions that you may have configured using "Taggit | Tag" should be changed
to now use "Extras | Tag."
### View Permissions
Django 2.1 introduced the ability to enforce view-only permissions for different object types. NetBox now enforces
these by default. You can grant view permission to a user or group by assigning the "can view" permission for the
desired object(s).
To exempt certain objects from the enforcement of view permissions, so that any user (including anonymous users) can
view them, add them to the new `EXEMPT_VIEW_PERMISSIONS` setting in `configuration.py`:
```
EXEMPT_VIEW_PERMISSIONS = [
'dcim.site',
'ipam.prefix',
]
```
To exclude _all_ objects, effectively disabling view permissions, set:
```
EXEMPT_VIEW_PERMISSIONS = ['*']
```
## Enhancements
* [#323](https://github.com/digitalocean/netbox/issues/323) - Enforce per-object type view permissions
* [#1792](https://github.com/digitalocean/netbox/issues/1792) - Add CustomFieldChoices API endpoint
* [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add `color` option for tags
* [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays

View File

@ -89,6 +89,30 @@ In order to send email, NetBox needs an email server configured. The following i
---
## EXEMPT_VIEW_PERMISSIONS
Default: Empty list
A list of models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users and by anonymous users.
List models in the form `<app>.<model>`. For example:
```
EXEMPT_VIEW_PERMISSIONS = [
'dcim.site',
'dcim.region',
'ipam.prefix',
]
```
To exempt _all_ models from view permission enforcement, set the following. (Note that `EXEMPT_VIEW_PERMISSIONS` must be an iterable.)
```
EXEMPT_VIEW_PERMISSIONS = ['*']
```
---
# ENFORCE_GLOBAL_UNIQUE
Default: False

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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):

View File

@ -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()

View File

@ -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):

View File

@ -55,16 +55,31 @@ class TokenPermissions(DjangoModelPermissions):
Custom permissions handler which extends the built-in DjangoModelPermissions to validate a Token's write ability
for unsafe requests (POST/PUT/PATCH/DELETE).
"""
# Override the stock perm_map to enforce view permissions
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': [],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def __init__(self):
# LOGIN_REQUIRED determines whether read-only access is provided to anonymous users.
self.authenticated_users_only = settings.LOGIN_REQUIRED
super().__init__()
def has_permission(self, request, view):
# If token authentication is in use, verify that the token allows write operations (for unsafe methods).
if request.method not in SAFE_METHODS and isinstance(request.auth, Token):
if not request.auth.write_enabled:
return False
return super().has_permission(request, view)

View File

@ -83,6 +83,14 @@ EMAIL = {
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False
# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
EXEMPT_VIEW_PERMISSIONS = [
# 'dcim.site',
# 'dcim.region',
# 'ipam.prefix',
]
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/1.11/topics/logging/
LOGGING = {}

View File

@ -51,8 +51,9 @@ CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EMAIL = getattr(configuration, 'EMAIL', {})
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
@ -93,7 +94,7 @@ if LDAP_CONFIGURED:
# Prepend LDAPBackend to the default ModelBackend
AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
'utilities.auth_backends.ViewExemptModelBackend',
]
# Optionally disable strict certificate checking
if LDAP_IGNORE_CERT_ERRORS:
@ -107,6 +108,10 @@ if LDAP_CONFIGURED:
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "
"netbox/ldap_config.py to disable LDAP."
)
else:
AUTHENTICATION_BACKENDS = [
'utilities.auth_backends.ViewExemptModelBackend',
]
# Database
configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})

View File

@ -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)

View File

@ -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'

View File

@ -45,9 +45,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ circuit.get_absolute_url }}">Circuit</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'circuits:circuit_changelog' pk=circuit.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'circuits:circuit_changelog' pk=circuit.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -51,9 +51,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ provider.get_absolute_url }}">Provider</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'circuits:provider_changelog' slug=provider.slug %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'circuits:provider_changelog' slug=provider.slug %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -27,9 +27,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ cable.get_absolute_url }}">Cable</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:cable_changelog' pk=cable.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:cable_changelog' pk=cable.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -86,12 +86,16 @@
{% include 'dcim/inc/device_napalm_tabs.html' %}
{% endif %}
{% endif %}
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_configcontext' pk=device.pk %}">Config Context</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_changelog' pk=device.pk %}">Changelog</a>
</li>
{% if perms.extras.view_configcontext %}
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_configcontext' pk=device.pk %}">Config Context</a>
</li>
{% endif %}
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_changelog' pk=device.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -50,9 +50,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ devicetype.get_absolute_url }}">Device Type</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:devicetype_changelog' pk=devicetype.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:devicetype_changelog' pk=devicetype.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -32,9 +32,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ interface.get_absolute_url }}">Interface</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:interface_changelog' pk=interface.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:interface_changelog' pk=interface.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -47,9 +47,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ rack.get_absolute_url }}">Rack</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:rack_changelog' pk=rack.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:rack_changelog' pk=rack.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -56,9 +56,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ site.get_absolute_url }}">Site</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:site_changelog' slug=site.slug %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'dcim:site_changelog' slug=site.slug %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -42,9 +42,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ tag.get_absolute_url }}">Tag</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'extras:tag_changelog' slug=tag.slug %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'extras:tag_changelog' slug=tag.slug %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -10,16 +10,20 @@
<strong>Organization</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.site_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4>
<p class="list-group-item-text text-muted">Geographic locations</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.tenant_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></h4>
<p class="list-group-item-text text-muted">Customers or departments</p>
</div>
{% if perms.dcim.view_site %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.site_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4>
<p class="list-group-item-text text-muted">Geographic locations</p>
</div>
{% endif %}
{% if perms.tenancy.view_tenant %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.tenant_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></h4>
<p class="list-group-item-text text-muted">Customers or departments</p>
</div>
{% endif %}
</div>
</div>
<div class="panel panel-default">
@ -27,26 +31,38 @@
<strong>DCIM</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.rack_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4>
<p class="list-group-item-text text-muted">Equipment racks, optionally organized by group</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.device_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:device_list' %}">Devices</a></h4>
<p class="list-group-item-text text-muted">Rack-mounted network equipment, servers, and other devices</p>
</div>
{% if perms.dcim.view_rack %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.rack_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4>
<p class="list-group-item-text text-muted">Equipment racks, optionally organized by group</p>
</div>
{% endif %}
{% if perms.dcim.view_device %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.device_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:device_list' %}">Devices</a></h4>
<p class="list-group-item-text text-muted">Rack-mounted network equipment, servers, and other devices</p>
</div>
{% endif %}
<div class="list-group-item">
<h4 class="list-group-item-heading">Connections</h4>
<span class="badge pull-right">{{ stats.cable_count }}</span>
<p style="padding-left: 20px;"><a href="{% url 'dcim:cable_list' %}">Cables</a></p>
<span class="badge pull-right">{{ stats.interface_connections_count }}</span>
<p style="padding-left: 20px;"><a href="{% url 'dcim:interface_connections_list' %}">Interfaces</a></p>
<span class="badge pull-right">{{ stats.console_connections_count }}</span>
<p style="padding-left: 20px;"><a href="{% url 'dcim:console_connections_list' %}">Console</a></p>
<span class="badge pull-right">{{ stats.power_connections_count }}</span>
<p class="list-group-item-text" style="padding-left: 20px;"><a href="{% url 'dcim:power_connections_list' %}">Power</a></p>
{% if perms.dcim.view_cable %}
<span class="badge pull-right">{{ stats.cable_count }}</span>
<p style="padding-left: 20px;"><a href="{% url 'dcim:cable_list' %}">Cables</a></p>
{% endif %}
{% if perms.dcim.view_interface %}
<span class="badge pull-right">{{ stats.interface_connections_count }}</span>
<p style="padding-left: 20px;"><a href="{% url 'dcim:interface_connections_list' %}">Interfaces</a></p>
{% endif %}
{% if perms.dcim.view_consoleport and perms.dcim.view_consoleserverport %}
<span class="badge pull-right">{{ stats.console_connections_count }}</span>
<p style="padding-left: 20px;"><a href="{% url 'dcim:console_connections_list' %}">Console</a></p>
{% endif %}
{% if perms.dcim.view_powerport and perms.dcim.view_poweroutlet %}
<span class="badge pull-right">{{ stats.power_connections_count }}</span>
<p class="list-group-item-text" style="padding-left: 20px;"><a href="{% url 'dcim:power_connections_list' %}">Power</a></p>
{% endif %}
</div>
</div>
</div>
@ -55,16 +71,20 @@
<strong>Virtualization</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.cluster_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:cluster_list' %}">Clusters</a></h4>
<p class="list-group-item-text text-muted">Clusters of physical hosts in which VMs reside</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.virtualmachine_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></h4>
<p class="list-group-item-text text-muted">Virtual compute instances running inside clusters</p>
</div>
{% if perms.virtualization.view_cluster %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.cluster_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:cluster_list' %}">Clusters</a></h4>
<p class="list-group-item-text text-muted">Clusters of physical hosts in which VMs reside</p>
</div>
{% endif %}
{% if perms.virtualization.view_virtualmachine %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.virtualmachine_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></h4>
<p class="list-group-item-text text-muted">Virtual compute instances running inside clusters</p>
</div>
{% endif %}
</div>
</div>
</div>
@ -74,31 +94,41 @@
<strong>IPAM</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.vrf_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></h4>
<p class="list-group-item-text text-muted">Virtual routing and forwarding tables</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.aggregate_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4>
<p class="list-group-item-text text-muted">Top-level IP allocations</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.prefix_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></h4>
<p class="list-group-item-text text-muted">IPv4 and IPv6 network assignments</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.ipaddress_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></h4>
<p class="list-group-item-text text-muted">Individual IPv4 and IPv6 addresses</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.vlan_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vlan_list' %}">VLANs</a></h4>
<p class="list-group-item-text text-muted">Layer two domains, identified by VLAN ID</p>
</div>
{% if perms.ipam.view_vrf %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.vrf_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></h4>
<p class="list-group-item-text text-muted">Virtual routing and forwarding tables</p>
</div>
{% endif %}
{% if perms.ipam.view_aggregate %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.aggregate_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4>
<p class="list-group-item-text text-muted">Top-level IP allocations</p>
</div>
{% endif %}
{% if perms.ipam.view_prefix %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.prefix_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></h4>
<p class="list-group-item-text text-muted">IPv4 and IPv6 network assignments</p>
</div>
{% endif %}
{% if perms.ipam.view_ipaddress %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.ipaddress_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></h4>
<p class="list-group-item-text text-muted">Individual IPv4 and IPv6 addresses</p>
</div>
{% endif %}
{% if perms.ipam.view_vlan %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.vlan_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vlan_list' %}">VLANs</a></h4>
<p class="list-group-item-text text-muted">Layer two domains, identified by VLAN ID</p>
</div>
{% endif %}
</div>
</div>
<div class="panel panel-default">
@ -106,16 +136,20 @@
<strong>Circuits</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.provider_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'circuits:provider_list' %}">Providers</a></h4>
<p class="list-group-item-text text-muted">Organizations which provide circuit connectivity</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.circuit_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'circuits:circuit_list' %}">Circuits</a></h4>
<p class="list-group-item-text text-muted">Communication links for Internet transit, peering, and other services</p>
</div>
{% if perms.circuits.view_provider %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.provider_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'circuits:provider_list' %}">Providers</a></h4>
<p class="list-group-item-text text-muted">Organizations which provide circuit connectivity</p>
</div>
{% endif %}
{% if perms.circuits.view_circuit %}
<div class="list-group-item">
<span class="badge pull-right">{{ stats.circuit_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'circuits:circuit_list' %}">Circuits</a></h4>
<p class="list-group-item-text text-muted">Communication links for Internet transit, peering, and other services</p>
</div>
{% endif %}
</div>
</div>
{% if perms.secrets %}
@ -134,26 +168,28 @@
{% endif %}
</div>
<div class="col-sm-6 col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Global Topology Maps</strong>
</div>
{% if topology_maps %}
<table class="table table-hover panel-body">
{% for tm in topology_maps %}
<tr>
<td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
<td>{{ tm.description }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body text-muted">
None
{% if perms.extras.view_topologymap %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Global Topology Maps</strong>
</div>
{% endif %}
</div>
{% if report_results %}
{% if topology_maps %}
<table class="table table-hover panel-body">
{% for tm in topology_maps %}
<tr>
<td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
<td>{{ tm.description }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body text-muted">
None
</div>
{% endif %}
</div>
{% endif %}
{% if report_results and perms.extras.view_reportresult %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Reports</strong>
@ -168,44 +204,46 @@
</table>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Changelog</strong>
{% if perms.extras.view_objectchange %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Changelog</strong>
</div>
<div class="list-group">
{% for change in changelog %}
{% with action=change.get_action_display|lower %}
<div class="list-group-item">
{% if action == 'created' %}
<span class="label label-success"><i class="fa fa-plus"></i></span>
{% elif action == 'updated' %}
<span class="label label-warning"><i class="fa fa-pencil"></i></span>
{% elif action == 'deleted' %}
<span class="label label-danger"><i class="fa fa-trash"></i></span>
{% endif %}
{{ change.changed_object_type.name|bettertitle }}
{% if change.changed_object.get_absolute_url %}
<a href="{{ change.changed_object.get_absolute_url }}">{{ change.changed_object }}</a>
{% else %}
{{ change.changed_object|default:change.object_repr }}
{% endif %}
<br />
<small>
<span class="text-muted">{{ change.user|default:change.user_name }} -</span>
<a href="{{ change.get_absolute_url }}" class="text-muted">{{ change.time|date:'SHORT_DATETIME_FORMAT' }}</a>
</small>
</div>
{% endwith %}
{% if forloop.last %}
<div class="list-group-item text-right">
<a href="{% url 'extras:objectchange_list' %}">View All Changes</a>
</div>
{% endif %}
{% empty %}
<div class="list-group-item text-muted">No change history found</div>
{% endfor %}
</div>
</div>
<div class="list-group">
{% for change in changelog %}
{% with action=change.get_action_display|lower %}
<div class="list-group-item">
{% if action == 'created' %}
<span class="label label-success"><i class="fa fa-plus"></i></span>
{% elif action == 'updated' %}
<span class="label label-warning"><i class="fa fa-pencil"></i></span>
{% elif action == 'deleted' %}
<span class="label label-danger"><i class="fa fa-trash"></i></span>
{% endif %}
{{ change.changed_object_type.name|bettertitle }}
{% if change.changed_object.get_absolute_url %}
<a href="{{ change.changed_object.get_absolute_url }}">{{ change.changed_object }}</a>
{% else %}
{{ change.changed_object|default:change.object_repr }}
{% endif %}
<br />
<small>
<span class="text-muted">{{ change.user|default:change.user_name }} -</span>
<a href="{{ change.get_absolute_url }}" class="text-muted">{{ change.time|date:'SHORT_DATETIME_FORMAT' }}</a>
</small>
</div>
{% endwith %}
{% if forloop.last %}
<div class="list-group-item text-right">
<a href="{% url 'extras:objectchange_list' %}">View All Changes</a>
</div>
{% endif %}
{% empty %}
<div class="list-group-item text-muted">No change history found</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -20,7 +20,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Sites</li>
<li>
<li{% if not perms.dcim.view_site %} class="disabled"{% endif %}>
{% if perms.dcim.add_site %}
<div class="buttons pull-right">
<a href="{% url 'dcim:site_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -29,7 +29,7 @@
{% endif %}
<a href="{% url 'dcim:site_list' %}">Sites</a>
</li>
<li>
<li{% if not perms.dcim.view_region %} class="disabled"{% endif %}>
{% if perms.dcim.add_region %}
<div class="buttons pull-right">
<a href="{% url 'dcim:region_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -40,7 +40,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Tenancy</li>
<li>
<li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
{% if perms.tenancy.add_tenant %}
<div class="buttons pull-right">
<a href="{% url 'tenancy:tenant_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -49,7 +49,7 @@
{% endif %}
<a href="{% url 'tenancy:tenant_list' %}">Tenants</a>
</li>
<li>
<li{% if not perms.tenancy.view_tenantgroup %} class="disabled"{% endif %}>
{% if perms.tenancy.add_tenantgroup %}
<div class="buttons pull-right">
<a href="{% url 'tenancy:tenantgroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -60,16 +60,16 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Miscellaneous</li>
<li>
<li{% if not perms.extras.view_tag %} class="disabled"{% endif %}>
<a href="{% url 'extras:tag_list' %}">Tags</a>
</li>
<li>
<li{% if not perms.extras.view_configcontext %} class="disabled"{% endif %}>
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
</li>
<li>
<li{% if not perms.extras.view_reportresult %} class="disabled"{% endif %}>
<a href="{% url 'extras:report_list' %}">Reports</a>
</li>
<li>
<li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
<a href="{% url 'extras:objectchange_list' %}">Changelog</a>
</li>
</ul>
@ -78,7 +78,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Racks</li>
<li>
<li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
{% if perms.dcim.add_rack %}
<div class="buttons pull-right">
<a href="{% url 'dcim:rack_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -87,7 +87,7 @@
{% endif %}
<a href="{% url 'dcim:rack_list' %}">Racks</a>
</li>
<li>
<li{% if not perms.dcim.view_rackgroup %} class="disabled"{% endif %}>
{% if perms.dcim.add_rackgroup %}
<div class="buttons pull-right">
<a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -96,7 +96,7 @@
{% endif %}
<a href="{% url 'dcim:rackgroup_list' %}">Rack Groups</a>
</li>
<li>
<li{% if not perms.dcim.view_rackrole %} class="disabled"{% endif %}>
{% if perms.dcim.add_rackrole %}
<div class="buttons pull-right">
<a href="{% url 'dcim:rackrole_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -105,10 +105,10 @@
{% endif %}
<a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a>
</li>
<li>
<li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
<a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
</li>
<li>
<li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
</li>
</ul>
@ -117,7 +117,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Devices</li>
<li>
<li{% if not perms.dcim.view_device %} class="disabled"{% endif %}>
{% if perms.dcim.add_device %}
<div class="buttons pull-right">
<a href="{% url 'dcim:device_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -126,7 +126,7 @@
{% endif %}
<a href="{% url 'dcim:device_list' %}">Devices</a>
</li>
<li>
<li{% if not perms.dcim.view_devicerole %} class="disabled"{% endif %}>
{% if perms.dcim.add_devicerole %}
<div class="buttons pull-right">
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -135,7 +135,7 @@
{% endif %}
<a href="{% url 'dcim:devicerole_list' %}">Device Roles</a>
</li>
<li>
<li{% if not perms.dcim.view_platform %} class="disabled"{% endif %}>
{% if perms.dcim.add_platform %}
<div class="buttons pull-right">
<a href="{% url 'dcim:platform_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -144,12 +144,12 @@
{% endif %}
<a href="{% url 'dcim:platform_list' %}">Platforms</a>
</li>
<li>
<li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}>
<a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
</li>
<li class="divider"></li>
<li class="dropdown-header">Device Types</li>
<li>
<li{% if not perms.dcim.view_devicetype %} class="disabled"{% endif %}>
{% if perms.dcim.add_devicetype %}
<div class="buttons pull-right">
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -158,7 +158,7 @@
{% endif %}
<a href="{% url 'dcim:devicetype_list' %}">Device Types</a>
</li>
<li>
<li{% if not perms.dcim.view_manufacturer %} class="disabled"{% endif %}>
{% if perms.dcim.add_manufacturer %}
<div class="buttons pull-right">
<a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -169,7 +169,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Inventory</li>
<li>
<li{% if not perms.dcim.view_inventoryitem %} class="disabled"{% endif %}>
{% if perms.dcim.add_inventoryitem %}
<div class="buttons pull-right">
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
@ -179,7 +179,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Connections</li>
<li>
<li{% if not perms.dcim.view_cable %} class="disabled"{% endif %}>
{% if perms.dcim.add_cable %}
<div class="buttons pull-right">
<a href="{% url 'dcim:cable_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
@ -187,13 +187,13 @@
{% endif %}
<a href="{% url 'dcim:cable_list' %}">Cables</a>
</li>
<li>
<li{% if not perms.dcim.view_consoleport or not perms.dcim.view_consoleserverport %} class="disabled"{% endif %}>
<a href="{% url 'dcim:console_connections_list' %}">Console Connections</a>
</li>
<li>
<li{% if not perms.dcim.view_powerport or not perms.dcim.view_poweroutlet %} class="disabled"{% endif %}>
<a href="{% url 'dcim:power_connections_list' %}">Power Connections</a>
</li>
<li>
<li{% if not perms.dcim.view_interface %} class="disabled"{% endif %}>
<a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a>
</li>
</ul>
@ -202,7 +202,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IPAM <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">IP Addresses</li>
<li>
<li{% if not perms.ipam.view_ipaddress %} class="disabled"{% endif %}>
{% if perms.ipam.add_ipaddress %}
<div class="buttons pull-right">
<a href="{% url 'ipam:ipaddress_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -213,7 +213,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Prefixes</li>
<li>
<li{% if not perms.ipam.view_prefix %} class="disabled"{% endif %}>
{% if perms.ipam.add_prefix %}
<div class="buttons pull-right">
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -222,7 +222,7 @@
{% endif %}
<a href="{% url 'ipam:prefix_list' %}">Prefixes</a>
</li>
<li>
<li{% if not perms.ipam.view_role %} class="disabled"{% endif %}>
{% if perms.ipam.add_role %}
<div class="buttons pull-right">
<a href="{% url 'ipam:role_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -233,7 +233,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Aggregates</li>
<li>
<li{% if not perms.ipam.view_aggregate %} class="disabled"{% endif %}>
{% if perms.ipam.add_aggregate %}
<div class="buttons pull-right">
<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -242,7 +242,7 @@
{% endif %}
<a href="{% url 'ipam:aggregate_list' %}">Aggregates</a>
</li>
<li>
<li{% if not perms.ipam.view_rir %} class="disabled"{% endif %}>
{% if perms.ipam.add_rir %}
<div class="buttons pull-right">
<a href="{% url 'ipam:rir_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -253,7 +253,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">VRFs</li>
<li>
<li{% if not perms.ipam.view_vrf %} class="disabled"{% endif %}>
{% if perms.ipam.add_vrf %}
<div class="buttons pull-right">
<a href="{% url 'ipam:vrf_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -264,7 +264,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">VLANs</li>
<li>
<li{% if not perms.ipam.view_vlan %} class="disabled"{% endif %}>
{% if perms.ipam.add_vlan %}
<div class="buttons pull-right">
<a href="{% url 'ipam:vlan_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -273,7 +273,7 @@
{% endif %}
<a href="{% url 'ipam:vlan_list' %}">VLANs</a>
</li>
<li>
<li{% if not perms.ipam.view_vlangroup %} class="disabled"{% endif %}>
{% if perms.ipam.add_vlangroup %}
<div class="buttons pull-right">
<a href="{% url 'ipam:vlangroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -284,7 +284,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Services</li>
<li>
<li{% if not perms.ipam.view_service %} class="disabled"{% endif %}>
<a href="{% url 'ipam:service_list' %}">Services</a>
</li>
</ul>
@ -293,7 +293,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Virtualization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Virtual Machines</li>
<li>
<li{% if not perms.virtualization.view_virtualmachine %} class="disabled"{% endif %}>
{% if perms.virtualization.add_virtualmachine %}
<div class="buttons pull-right">
<a href="{% url 'virtualization:virtualmachine_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -304,7 +304,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Clusters</li>
<li>
<li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>
{% if perms.virtualization.add_cluster %}
<div class="buttons pull-right">
<a href="{% url 'virtualization:cluster_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -313,7 +313,7 @@
{% endif %}
<a href="{% url 'virtualization:cluster_list' %}">Clusters</a>
</li>
<li>
<li{% if not perms.virtualization.view_clustertype %} class="disabled"{% endif %}>
{% if perms.virtualization.add_clustertype %}
<div class="buttons pull-right">
<a href="{% url 'virtualization:clustertype_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -322,7 +322,7 @@
{% endif %}
<a href="{% url 'virtualization:clustertype_list' %}">Cluster Types</a>
</li>
<li>
<li{% if not perms.virtualization.view_clustergroup %} class="disabled"{% endif %}>
{% if perms.virtualization.add_clustergroup %}
<div class="buttons pull-right">
<a href="{% url 'virtualization:clustergroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -337,7 +337,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Circuits</li>
<li>
<li{% if not perms.circuits.view_circuit %} class="disabled"{% endif %}>
{% if perms.circuits.add_circuit %}
<div class="buttons pull-right">
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -346,7 +346,7 @@
{% endif %}
<a href="{% url 'circuits:circuit_list' %}">Circuits</a>
</li>
<li>
<li{% if not perms.circuits.view_circuittype %} class="disabled"{% endif %}>
{% if perms.circuits.add_circuittype %}
<div class="buttons pull-right">
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -357,7 +357,7 @@
</li>
<li class="divider"></li>
<li class="dropdown-header">Providers</li>
<li>
<li{% if not perms.circuits.view_provider %} class="disabled"{% endif %}>
{% if perms.circuits.add_provider %}
<div class="buttons pull-right">
<a href="{% url 'circuits:provider_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -371,7 +371,7 @@
<li class="dropdown{% if request.path|contains:'/dcim/power' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Power <span class="caret"></span></a>
<ul class="dropdown-menu">
<li>
<li{% if not perms.dcim.view_powerfeed %} class="disabled"{% endif %}>
{% if perms.dcim.add_powerfeed %}
<div class="buttons pull-right">
<a href="{% url 'dcim:powerfeed_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
@ -380,7 +380,7 @@
{% endif %}
<a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a>
</li>
<li>
<li{% if not perms.dcim.view_powerpanel %} class="disabled"{% endif %}>
{% if perms.dcim.add_powerpanel %}
<div class="buttons pull-right">
<a href="{% url 'dcim:powerpanel_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>

View File

@ -43,9 +43,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ aggregate.get_absolute_url }}">Aggregate</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:aggregate_changelog' pk=aggregate.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:aggregate_changelog' pk=aggregate.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -45,9 +45,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ ipaddress.get_absolute_url }}">IP Address</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:ipaddress_changelog' pk=ipaddress.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:ipaddress_changelog' pk=ipaddress.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -59,12 +59,16 @@
<li role="presentation"{% if active_tab == 'prefixes' %} class="active"{% endif %}>
<a href="{% url 'ipam:prefix_prefixes' pk=prefix.pk %}">Child Prefixes <span class="badge">{{ prefix.get_child_prefixes.count }}</span></a>
</li>
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}>
<a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:prefix_changelog' pk=prefix.pk %}">Changelog</a>
</li>
{% if perms.ipam.view_ipaddress %}
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}>
<a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a>
</li>
{% endif %}
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:prefix_changelog' pk=prefix.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -51,9 +51,11 @@
<li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}>
<a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:vlan_changelog' pk=vlan.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:vlan_changelog' pk=vlan.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -42,9 +42,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ vrf.get_absolute_url }}">VRF</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -33,9 +33,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ secret.get_absolute_url }}">Secret</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'secrets:secret_changelog' pk=secret.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'secrets:secret_changelog' pk=secret.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -45,9 +45,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ tenant.get_absolute_url }}">Tenant</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'tenancy:tenant_changelog' slug=tenant.slug %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'tenancy:tenant_changelog' slug=tenant.slug %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -45,9 +45,11 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ cluster.get_absolute_url }}">Cluster</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'virtualization:cluster_changelog' pk=cluster.pk %}">Changelog</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'virtualization:cluster_changelog' pk=cluster.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -44,12 +44,16 @@
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ virtualmachine.get_absolute_url }}">Virtual Machine</a>
</li>
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>
<a href="{% url 'virtualization:virtualmachine_configcontext' pk=virtualmachine.pk %}">Config Context</a>
</li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'virtualization:virtualmachine_changelog' pk=virtualmachine.pk %}">Changelog</a>
</li>
{% if perms.extras.view_configcontext %}
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>
<a href="{% url 'virtualization:virtualmachine_configcontext' pk=virtualmachine.pk %}">Config Context</a>
</li>
{% endif %}
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'virtualization:virtualmachine_changelog' pk=virtualmachine.pk %}">Changelog</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -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()

View File

@ -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):

View File

@ -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):

View File

@ -0,0 +1,28 @@
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
class ViewExemptModelBackend(ModelBackend):
"""
Custom implementation of Django's stock ModelBackend which allows for the exemption of arbitrary models from view
permission enforcement.
"""
def has_perm(self, user_obj, perm, obj=None):
# If this is a view permission, check whether the model has been exempted from enforcement
try:
app, codename = perm.split('.')
action, model = codename.split('_')
if action == 'view':
if (
# All models are exempt from view permission enforcement
'*' in settings.EXEMPT_VIEW_PERMISSIONS
) or (
# This specific model is exempt from view permission enforcement
'{}.{}'.format(app, model) in settings.EXEMPT_VIEW_PERMISSIONS
):
return True
except ValueError:
pass
return super().has_perm(user_obj, perm, obj)

View File

@ -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

View File

@ -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()

View File

@ -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'