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 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." 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 ## 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 * [#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 * [#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 * [#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 # ENFORCE_GLOBAL_UNIQUE
Default: False Default: False

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 Custom permissions handler which extends the built-in DjangoModelPermissions to validate a Token's write ability
for unsafe requests (POST/PUT/PATCH/DELETE). 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): def __init__(self):
# LOGIN_REQUIRED determines whether read-only access is provided to anonymous users. # LOGIN_REQUIRED determines whether read-only access is provided to anonymous users.
self.authenticated_users_only = settings.LOGIN_REQUIRED self.authenticated_users_only = settings.LOGIN_REQUIRED
super().__init__() super().__init__()
def has_permission(self, request, view): def has_permission(self, request, view):
# If token authentication is in use, verify that the token allows write operations (for unsafe methods). # 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 request.method not in SAFE_METHODS and isinstance(request.auth, Token):
if not request.auth.write_enabled: if not request.auth.write_enabled:
return False return False
return super().has_permission(request, view) 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. # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False 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: # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/1.11/topics/logging/ # https://docs.djangoproject.com/en/1.11/topics/logging/
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') DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False) DEBUG = getattr(configuration, 'DEBUG', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EMAIL = getattr(configuration, 'EMAIL', {}) 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', {}) LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False) LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None) LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
@ -93,7 +94,7 @@ if LDAP_CONFIGURED:
# Prepend LDAPBackend to the default ModelBackend # Prepend LDAPBackend to the default ModelBackend
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend', 'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend', 'utilities.auth_backends.ViewExemptModelBackend',
] ]
# Optionally disable strict certificate checking # Optionally disable strict certificate checking
if LDAP_IGNORE_CERT_ERRORS: 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 " "LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "
"netbox/ldap_config.py to disable LDAP." "netbox/ldap_config.py to disable LDAP."
) )
else:
AUTHENTICATION_BACKENDS = [
'utilities.auth_backends.ViewExemptModelBackend',
]
# Database # Database
configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'}) configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,16 +10,20 @@
<strong>Organization</strong> <strong>Organization</strong>
</div> </div>
<div class="list-group"> <div class="list-group">
<div class="list-group-item"> {% if perms.dcim.view_site %}
<span class="badge pull-right">{{ stats.site_count }}</span> <div class="list-group-item">
<h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4> <span class="badge pull-right">{{ stats.site_count }}</span>
<p class="list-group-item-text text-muted">Geographic locations</p> <h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4>
</div> <p class="list-group-item-text text-muted">Geographic locations</p>
<div class="list-group-item"> </div>
<span class="badge pull-right">{{ stats.tenant_count }}</span> {% endif %}
<h4 class="list-group-item-heading"><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></h4> {% if perms.tenancy.view_tenant %}
<p class="list-group-item-text text-muted">Customers or departments</p> <div class="list-group-item">
</div> <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> </div>
<div class="panel panel-default"> <div class="panel panel-default">
@ -27,26 +31,38 @@
<strong>DCIM</strong> <strong>DCIM</strong>
</div> </div>
<div class="list-group"> <div class="list-group">
<div class="list-group-item"> {% if perms.dcim.view_rack %}
<span class="badge pull-right">{{ stats.rack_count }}</span> <div class="list-group-item">
<h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4> <span class="badge pull-right">{{ stats.rack_count }}</span>
<p class="list-group-item-text text-muted">Equipment racks, optionally organized by group</p> <h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4>
</div> <p class="list-group-item-text text-muted">Equipment racks, optionally organized by group</p>
<div class="list-group-item"> </div>
<span class="badge pull-right">{{ stats.device_count }}</span> {% endif %}
<h4 class="list-group-item-heading"><a href="{% url 'dcim:device_list' %}">Devices</a></h4> {% if perms.dcim.view_device %}
<p class="list-group-item-text text-muted">Rack-mounted network equipment, servers, and other devices</p> <div class="list-group-item">
</div> <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"> <div class="list-group-item">
<h4 class="list-group-item-heading">Connections</h4> <h4 class="list-group-item-heading">Connections</h4>
<span class="badge pull-right">{{ stats.cable_count }}</span> {% if perms.dcim.view_cable %}
<p style="padding-left: 20px;"><a href="{% url 'dcim:cable_list' %}">Cables</a></p> <span class="badge pull-right">{{ stats.cable_count }}</span>
<span class="badge pull-right">{{ stats.interface_connections_count }}</span> <p style="padding-left: 20px;"><a href="{% url 'dcim:cable_list' %}">Cables</a></p>
<p style="padding-left: 20px;"><a href="{% url 'dcim:interface_connections_list' %}">Interfaces</a></p> {% endif %}
<span class="badge pull-right">{{ stats.console_connections_count }}</span> {% if perms.dcim.view_interface %}
<p style="padding-left: 20px;"><a href="{% url 'dcim:console_connections_list' %}">Console</a></p> <span class="badge pull-right">{{ stats.interface_connections_count }}</span>
<span class="badge pull-right">{{ stats.power_connections_count }}</span> <p style="padding-left: 20px;"><a href="{% url 'dcim:interface_connections_list' %}">Interfaces</a></p>
<p class="list-group-item-text" style="padding-left: 20px;"><a href="{% url 'dcim:power_connections_list' %}">Power</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> </div>
</div> </div>
@ -55,16 +71,20 @@
<strong>Virtualization</strong> <strong>Virtualization</strong>
</div> </div>
<div class="list-group"> <div class="list-group">
<div class="list-group-item"> {% if perms.virtualization.view_cluster %}
<span class="badge pull-right">{{ stats.cluster_count }}</span> <div class="list-group-item">
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:cluster_list' %}">Clusters</a></h4> <span class="badge pull-right">{{ stats.cluster_count }}</span>
<p class="list-group-item-text text-muted">Clusters of physical hosts in which VMs reside</p> <h4 class="list-group-item-heading"><a href="{% url 'virtualization:cluster_list' %}">Clusters</a></h4>
</div> <p class="list-group-item-text text-muted">Clusters of physical hosts in which VMs reside</p>
<div class="list-group-item"> </div>
<span class="badge pull-right">{{ stats.virtualmachine_count }}</span> {% endif %}
<h4 class="list-group-item-heading"><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></h4> {% if perms.virtualization.view_virtualmachine %}
<p class="list-group-item-text text-muted">Virtual compute instances running inside clusters</p> <div class="list-group-item">
</div> <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> </div>
</div> </div>
@ -74,31 +94,41 @@
<strong>IPAM</strong> <strong>IPAM</strong>
</div> </div>
<div class="list-group"> <div class="list-group">
<div class="list-group-item"> {% if perms.ipam.view_vrf %}
<span class="badge pull-right">{{ stats.vrf_count }}</span> <div class="list-group-item">
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></h4> <span class="badge pull-right">{{ stats.vrf_count }}</span>
<p class="list-group-item-text text-muted">Virtual routing and forwarding tables</p> <h4 class="list-group-item-heading"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></h4>
</div> <p class="list-group-item-text text-muted">Virtual routing and forwarding tables</p>
<div class="list-group-item"> </div>
<span class="badge pull-right">{{ stats.aggregate_count }}</span> {% endif %}
<h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4> {% if perms.ipam.view_aggregate %}
<p class="list-group-item-text text-muted">Top-level IP allocations</p> <div class="list-group-item">
</div> <span class="badge pull-right">{{ stats.aggregate_count }}</span>
<div class="list-group-item"> <h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4>
<span class="badge pull-right">{{ stats.prefix_count }}</span> <p class="list-group-item-text text-muted">Top-level IP allocations</p>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></h4> </div>
<p class="list-group-item-text text-muted">IPv4 and IPv6 network assignments</p> {% endif %}
</div> {% if perms.ipam.view_prefix %}
<div class="list-group-item"> <div class="list-group-item">
<span class="badge pull-right">{{ stats.ipaddress_count }}</span> <span class="badge pull-right">{{ stats.prefix_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></h4> <h4 class="list-group-item-heading"><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></h4>
<p class="list-group-item-text text-muted">Individual IPv4 and IPv6 addresses</p> <p class="list-group-item-text text-muted">IPv4 and IPv6 network assignments</p>
</div> </div>
<div class="list-group-item"> {% endif %}
<span class="badge pull-right">{{ stats.vlan_count }}</span> {% if perms.ipam.view_ipaddress %}
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vlan_list' %}">VLANs</a></h4> <div class="list-group-item">
<p class="list-group-item-text text-muted">Layer two domains, identified by VLAN ID</p> <span class="badge pull-right">{{ stats.ipaddress_count }}</span>
</div> <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> </div>
<div class="panel panel-default"> <div class="panel panel-default">
@ -106,16 +136,20 @@
<strong>Circuits</strong> <strong>Circuits</strong>
</div> </div>
<div class="list-group"> <div class="list-group">
<div class="list-group-item"> {% if perms.circuits.view_provider %}
<span class="badge pull-right">{{ stats.provider_count }}</span> <div class="list-group-item">
<h4 class="list-group-item-heading"><a href="{% url 'circuits:provider_list' %}">Providers</a></h4> <span class="badge pull-right">{{ stats.provider_count }}</span>
<p class="list-group-item-text text-muted">Organizations which provide circuit connectivity</p> <h4 class="list-group-item-heading"><a href="{% url 'circuits:provider_list' %}">Providers</a></h4>
</div> <p class="list-group-item-text text-muted">Organizations which provide circuit connectivity</p>
<div class="list-group-item"> </div>
<span class="badge pull-right">{{ stats.circuit_count }}</span> {% endif %}
<h4 class="list-group-item-heading"><a href="{% url 'circuits:circuit_list' %}">Circuits</a></h4> {% if perms.circuits.view_circuit %}
<p class="list-group-item-text text-muted">Communication links for Internet transit, peering, and other services</p> <div class="list-group-item">
</div> <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>
</div> </div>
{% if perms.secrets %} {% if perms.secrets %}
@ -134,26 +168,28 @@
{% endif %} {% endif %}
</div> </div>
<div class="col-sm-6 col-md-4"> <div class="col-sm-6 col-md-4">
<div class="panel panel-default"> {% if perms.extras.view_topologymap %}
<div class="panel-heading"> <div class="panel panel-default">
<strong>Global Topology Maps</strong> <div class="panel-heading">
</div> <strong>Global Topology Maps</strong>
{% 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> </div>
{% endif %} {% if topology_maps %}
</div> <table class="table table-hover panel-body">
{% if report_results %} {% 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 panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Reports</strong> <strong>Reports</strong>
@ -168,44 +204,46 @@
</table> </table>
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default"> {% if perms.extras.view_objectchange %}
<div class="panel-heading"> <div class="panel panel-default">
<strong>Changelog</strong> <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>
<div class="list-group"> {% endif %}
{% 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> </div>
</div> </div>
{% endblock %} {% 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> <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"> <ul class="dropdown-menu">
<li class="dropdown-header">Sites</li> <li class="dropdown-header">Sites</li>
<li> <li{% if not perms.dcim.view_site %} class="disabled"{% endif %}>
{% if perms.dcim.add_site %} {% if perms.dcim.add_site %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:site_list' %}">Sites</a> <a href="{% url 'dcim:site_list' %}">Sites</a>
</li> </li>
<li> <li{% if not perms.dcim.view_region %} class="disabled"{% endif %}>
{% if perms.dcim.add_region %} {% if perms.dcim.add_region %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Tenancy</li> <li class="dropdown-header">Tenancy</li>
<li> <li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
{% if perms.tenancy.add_tenant %} {% if perms.tenancy.add_tenant %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'tenancy:tenant_list' %}">Tenants</a> <a href="{% url 'tenancy:tenant_list' %}">Tenants</a>
</li> </li>
<li> <li{% if not perms.tenancy.view_tenantgroup %} class="disabled"{% endif %}>
{% if perms.tenancy.add_tenantgroup %} {% if perms.tenancy.add_tenantgroup %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Miscellaneous</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> <a href="{% url 'extras:tag_list' %}">Tags</a>
</li> </li>
<li> <li{% if not perms.extras.view_configcontext %} class="disabled"{% endif %}>
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a> <a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
</li> </li>
<li> <li{% if not perms.extras.view_reportresult %} class="disabled"{% endif %}>
<a href="{% url 'extras:report_list' %}">Reports</a> <a href="{% url 'extras:report_list' %}">Reports</a>
</li> </li>
<li> <li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
<a href="{% url 'extras:objectchange_list' %}">Changelog</a> <a href="{% url 'extras:objectchange_list' %}">Changelog</a>
</li> </li>
</ul> </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> <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"> <ul class="dropdown-menu">
<li class="dropdown-header">Racks</li> <li class="dropdown-header">Racks</li>
<li> <li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
{% if perms.dcim.add_rack %} {% if perms.dcim.add_rack %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:rack_list' %}">Racks</a> <a href="{% url 'dcim:rack_list' %}">Racks</a>
</li> </li>
<li> <li{% if not perms.dcim.view_rackgroup %} class="disabled"{% endif %}>
{% if perms.dcim.add_rackgroup %} {% if perms.dcim.add_rackgroup %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:rackgroup_list' %}">Rack Groups</a> <a href="{% url 'dcim:rackgroup_list' %}">Rack Groups</a>
</li> </li>
<li> <li{% if not perms.dcim.view_rackrole %} class="disabled"{% endif %}>
{% if perms.dcim.add_rackrole %} {% if perms.dcim.add_rackrole %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a> <a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a>
</li> </li>
<li> <li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
<a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a> <a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
</li> </li>
<li> <li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a> <a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
</li> </li>
</ul> </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> <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"> <ul class="dropdown-menu">
<li class="dropdown-header">Devices</li> <li class="dropdown-header">Devices</li>
<li> <li{% if not perms.dcim.view_device %} class="disabled"{% endif %}>
{% if perms.dcim.add_device %} {% if perms.dcim.add_device %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:device_list' %}">Devices</a> <a href="{% url 'dcim:device_list' %}">Devices</a>
</li> </li>
<li> <li{% if not perms.dcim.view_devicerole %} class="disabled"{% endif %}>
{% if perms.dcim.add_devicerole %} {% if perms.dcim.add_devicerole %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:devicerole_list' %}">Device Roles</a> <a href="{% url 'dcim:devicerole_list' %}">Device Roles</a>
</li> </li>
<li> <li{% if not perms.dcim.view_platform %} class="disabled"{% endif %}>
{% if perms.dcim.add_platform %} {% if perms.dcim.add_platform %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:platform_list' %}">Platforms</a> <a href="{% url 'dcim:platform_list' %}">Platforms</a>
</li> </li>
<li> <li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}>
<a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a> <a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
</li> </li>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Device Types</li> <li class="dropdown-header">Device Types</li>
<li> <li{% if not perms.dcim.view_devicetype %} class="disabled"{% endif %}>
{% if perms.dcim.add_devicetype %} {% if perms.dcim.add_devicetype %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:devicetype_list' %}">Device Types</a> <a href="{% url 'dcim:devicetype_list' %}">Device Types</a>
</li> </li>
<li> <li{% if not perms.dcim.view_manufacturer %} class="disabled"{% endif %}>
{% if perms.dcim.add_manufacturer %} {% if perms.dcim.add_manufacturer %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Inventory</li> <li class="dropdown-header">Inventory</li>
<li> <li{% if not perms.dcim.view_inventoryitem %} class="disabled"{% endif %}>
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Connections</li> <li class="dropdown-header">Connections</li>
<li> <li{% if not perms.dcim.view_cable %} class="disabled"{% endif %}>
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:cable_list' %}">Cables</a> <a href="{% url 'dcim:cable_list' %}">Cables</a>
</li> </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> <a href="{% url 'dcim:console_connections_list' %}">Console Connections</a>
</li> </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> <a href="{% url 'dcim:power_connections_list' %}">Power Connections</a>
</li> </li>
<li> <li{% if not perms.dcim.view_interface %} class="disabled"{% endif %}>
<a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a> <a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a>
</li> </li>
</ul> </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> <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"> <ul class="dropdown-menu">
<li class="dropdown-header">IP Addresses</li> <li class="dropdown-header">IP Addresses</li>
<li> <li{% if not perms.ipam.view_ipaddress %} class="disabled"{% endif %}>
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Prefixes</li> <li class="dropdown-header">Prefixes</li>
<li> <li{% if not perms.ipam.view_prefix %} class="disabled"{% endif %}>
{% if perms.ipam.add_prefix %} {% if perms.ipam.add_prefix %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'ipam:prefix_list' %}">Prefixes</a> <a href="{% url 'ipam:prefix_list' %}">Prefixes</a>
</li> </li>
<li> <li{% if not perms.ipam.view_role %} class="disabled"{% endif %}>
{% if perms.ipam.add_role %} {% if perms.ipam.add_role %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Aggregates</li> <li class="dropdown-header">Aggregates</li>
<li> <li{% if not perms.ipam.view_aggregate %} class="disabled"{% endif %}>
{% if perms.ipam.add_aggregate %} {% if perms.ipam.add_aggregate %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'ipam:aggregate_list' %}">Aggregates</a> <a href="{% url 'ipam:aggregate_list' %}">Aggregates</a>
</li> </li>
<li> <li{% if not perms.ipam.view_rir %} class="disabled"{% endif %}>
{% if perms.ipam.add_rir %} {% if perms.ipam.add_rir %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">VRFs</li> <li class="dropdown-header">VRFs</li>
<li> <li{% if not perms.ipam.view_vrf %} class="disabled"{% endif %}>
{% if perms.ipam.add_vrf %} {% if perms.ipam.add_vrf %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">VLANs</li> <li class="dropdown-header">VLANs</li>
<li> <li{% if not perms.ipam.view_vlan %} class="disabled"{% endif %}>
{% if perms.ipam.add_vlan %} {% if perms.ipam.add_vlan %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'ipam:vlan_list' %}">VLANs</a> <a href="{% url 'ipam:vlan_list' %}">VLANs</a>
</li> </li>
<li> <li{% if not perms.ipam.view_vlangroup %} class="disabled"{% endif %}>
{% if perms.ipam.add_vlangroup %} {% if perms.ipam.add_vlangroup %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Services</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> <a href="{% url 'ipam:service_list' %}">Services</a>
</li> </li>
</ul> </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> <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"> <ul class="dropdown-menu">
<li class="dropdown-header">Virtual Machines</li> <li class="dropdown-header">Virtual Machines</li>
<li> <li{% if not perms.virtualization.view_virtualmachine %} class="disabled"{% endif %}>
{% if perms.virtualization.add_virtualmachine %} {% if perms.virtualization.add_virtualmachine %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Clusters</li> <li class="dropdown-header">Clusters</li>
<li> <li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>
{% if perms.virtualization.add_cluster %} {% if perms.virtualization.add_cluster %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'virtualization:cluster_list' %}">Clusters</a> <a href="{% url 'virtualization:cluster_list' %}">Clusters</a>
</li> </li>
<li> <li{% if not perms.virtualization.view_clustertype %} class="disabled"{% endif %}>
{% if perms.virtualization.add_clustertype %} {% if perms.virtualization.add_clustertype %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'virtualization:clustertype_list' %}">Cluster Types</a> <a href="{% url 'virtualization:clustertype_list' %}">Cluster Types</a>
</li> </li>
<li> <li{% if not perms.virtualization.view_clustergroup %} class="disabled"{% endif %}>
{% if perms.virtualization.add_clustergroup %} {% if perms.virtualization.add_clustergroup %}
<div class="buttons pull-right"> <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> <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> <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"> <ul class="dropdown-menu">
<li class="dropdown-header">Circuits</li> <li class="dropdown-header">Circuits</li>
<li> <li{% if not perms.circuits.view_circuit %} class="disabled"{% endif %}>
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'circuits:circuit_list' %}">Circuits</a> <a href="{% url 'circuits:circuit_list' %}">Circuits</a>
</li> </li>
<li> <li{% if not perms.circuits.view_circuittype %} class="disabled"{% endif %}>
{% if perms.circuits.add_circuittype %} {% if perms.circuits.add_circuittype %}
<div class="buttons pull-right"> <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> <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>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Providers</li> <li class="dropdown-header">Providers</li>
<li> <li{% if not perms.circuits.view_provider %} class="disabled"{% endif %}>
{% if perms.circuits.add_provider %} {% if perms.circuits.add_provider %}
<div class="buttons pull-right"> <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> <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 %}"> <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> <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"> <ul class="dropdown-menu">
<li> <li{% if not perms.dcim.view_powerfeed %} class="disabled"{% endif %}>
{% if perms.dcim.add_powerfeed %} {% if perms.dcim.add_powerfeed %}
<div class="buttons pull-right"> <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> <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 %} {% endif %}
<a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a> <a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a>
</li> </li>
<li> <li{% if not perms.dcim.view_powerpanel %} class="disabled"{% endif %}>
{% if perms.dcim.add_powerpanel %} {% if perms.dcim.add_powerpanel %}
<div class="buttons pull-right"> <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> <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 %}> <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ aggregate.get_absolute_url }}">Aggregate</a> <a href="{{ aggregate.get_absolute_url }}">Aggregate</a>
</li> </li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}> {% if perms.extras.view_objectchange %}
<a href="{% url 'ipam:aggregate_changelog' pk=aggregate.pk %}">Changelog</a> <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
</li> <a href="{% url 'ipam:aggregate_changelog' pk=aggregate.pk %}">Changelog</a>
</li>
{% endif %}
</ul> </ul>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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