1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Introduce ObjectView to enforce object-level permissions for individual object views

This commit is contained in:
Jeremy Stretch
2020-05-21 15:39:07 -04:00
parent 8fd860a413
commit e61fc1f709
9 changed files with 118 additions and 106 deletions

View File

@ -4,6 +4,10 @@ Utility views are reusable views that handle common CRUD tasks, such as listing
## Individual Views
### ObjectView
Retrieve and display a single object.
### ObjectListView
Generates a paginated table of objects from a given queryset, which may optionally be filtered.

View File

@ -1,18 +1,16 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.db.models import Count, OuterRef
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View
from django_tables2 import RequestConfig
from extras.models import Graph
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .choices import CircuitTerminationSideChoices
@ -30,12 +28,12 @@ class ProviderListView(ObjectListView):
table = tables.ProviderTable
class ProviderView(PermissionRequiredMixin, View):
permission_required = 'circuits.view_provider'
class ProviderView(ObjectView):
queryset = Provider.objects.all()
def get(self, request, slug):
provider = get_object_or_404(Provider, slug=slug)
provider = get_object_or_404(self.queryset, slug=slug)
circuits = Circuit.objects.filter(
provider=provider
).prefetch_related(
@ -135,12 +133,12 @@ class CircuitListView(ObjectListView):
table = tables.CircuitTable
class CircuitView(PermissionRequiredMixin, View):
permission_required = 'circuits.view_circuit'
class CircuitView(ObjectView):
queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant__group')
def get(self, request, pk):
circuit = get_object_or_404(Circuit.objects.prefetch_related('provider', 'type', 'tenant__group'), pk=pk)
circuit = get_object_or_404(self.queryset, pk=pk)
termination_a = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device'
).filter(

View File

@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
from utilities.utils import csv_format
from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin,
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
@ -185,8 +185,7 @@ class SiteListView(ObjectListView):
table = tables.SiteTable
class SiteView(ObjectPermissionRequiredMixin, View):
permission_required = 'dcim.view_site'
class SiteView(ObjectView):
queryset = Site.objects.prefetch_related('region', 'tenant__group')
def get(self, request, slug):
@ -362,12 +361,12 @@ class RackElevationListView(PermissionRequiredMixin, View):
})
class RackView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_rack'
class RackView(ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')
def get(self, request, pk):
rack = get_object_or_404(Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
rack = get_object_or_404(self.queryset, pk=pk)
nonracked_devices = Device.objects.filter(
rack=rack,
@ -440,12 +439,12 @@ class RackReservationListView(ObjectListView):
action_buttons = ('export',)
class RackReservationView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_rackreservation'
class RackReservationView(ObjectView):
queryset = RackReservation.objects.prefetch_related('rack')
def get(self, request, pk):
rackreservation = get_object_or_404(RackReservation.objects.prefetch_related('rack'), pk=pk)
rackreservation = get_object_or_404(self.queryset, pk=pk)
return render(request, 'dcim/rackreservation.html', {
'rackreservation': rackreservation,
@ -546,12 +545,12 @@ class DeviceTypeListView(ObjectListView):
table = tables.DeviceTypeTable
class DeviceTypeView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_devicetype'
class DeviceTypeView(ObjectView):
queryset = DeviceType.objects.prefetch_related('manufacturer')
def get(self, request, pk):
devicetype = get_object_or_404(DeviceType, pk=pk)
devicetype = get_object_or_404(self.queryset, pk=pk)
# Component tables
consoleport_table = tables.ConsolePortTemplateTable(
@ -990,14 +989,14 @@ class DeviceListView(ObjectListView):
template_name = 'dcim/device_list.html'
class DeviceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_device'
class DeviceView(ObjectView):
queryset = Device.objects.prefetch_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
)
def get(self, request, pk):
device = get_object_or_404(Device.objects.prefetch_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
), pk=pk)
device = get_object_or_404(self.queryset, pk=pk)
# VirtualChassis members
if device.virtual_chassis is not None:
@ -1068,12 +1067,12 @@ class DeviceView(PermissionRequiredMixin, View):
})
class DeviceInventoryView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_device'
class DeviceInventoryView(ObjectView):
queryset = Device.objects.all()
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
device = get_object_or_404(self.queryset, pk=pk)
inventory_items = InventoryItem.objects.filter(
device=device, parent=None
).prefetch_related(
@ -1087,12 +1086,13 @@ class DeviceInventoryView(PermissionRequiredMixin, View):
})
class DeviceStatusView(PermissionRequiredMixin, View):
class DeviceStatusView(ObjectView):
permission_required = ('dcim.view_device', 'dcim.napalm_read')
queryset = Device.objects.all()
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
device = get_object_or_404(self.queryset, pk=pk)
return render(request, 'dcim/device_status.html', {
'device': device,
@ -1102,10 +1102,11 @@ class DeviceStatusView(PermissionRequiredMixin, View):
class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_device', 'dcim.napalm_read')
queryset = Device.objects.all()
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
device = get_object_or_404(self.queryset, pk=pk)
interfaces = device.vc_interfaces.exclude(type__in=NONCONNECTABLE_IFACE_TYPES).prefetch_related(
'_connected_interface__device'
)
@ -1119,10 +1120,11 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
class DeviceConfigView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_device', 'dcim.napalm_read')
queryset = Device.objects.all()
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
device = get_object_or_404(self.queryset, pk=pk)
return render(request, 'dcim/device_config.html', {
'device': device,
@ -1426,12 +1428,12 @@ class InterfaceListView(ObjectListView):
action_buttons = ('import', 'export')
class InterfaceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_interface'
class InterfaceView(ObjectView):
queryset = Interface.objects.all()
def get(self, request, pk):
interface = get_object_or_404(Interface, pk=pk)
interface = get_object_or_404(self.queryset, pk=pk)
# Get assigned IP addresses
ipaddress_table = InterfaceIPAddressTable(
@ -1878,12 +1880,12 @@ class CableListView(ObjectListView):
action_buttons = ('import', 'export')
class CableView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_cable'
class CableView(ObjectView):
queryset = Cable.objects.all()
def get(self, request, pk):
cable = get_object_or_404(Cable, pk=pk)
cable = get_object_or_404(self.queryset, pk=pk)
return render(request, 'dcim/cable.html', {
'cable': cable,
@ -2194,11 +2196,11 @@ class VirtualChassisListView(ObjectListView):
action_buttons = ('export',)
class VirtualChassisView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_virtualchassis'
class VirtualChassisView(ObjectView):
queryset = VirtualChassis.objects.prefetch_related('members')
def get(self, request, pk):
virtualchassis = get_object_or_404(VirtualChassis.objects.prefetch_related('members'), pk=pk)
virtualchassis = get_object_or_404(self.queryset, pk=pk)
return render(request, 'dcim/virtualchassis.html', {
'virtualchassis': virtualchassis,
@ -2461,12 +2463,12 @@ class PowerPanelListView(ObjectListView):
table = tables.PowerPanelTable
class PowerPanelView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_powerpanel'
class PowerPanelView(ObjectView):
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
def get(self, request, pk):
powerpanel = get_object_or_404(PowerPanel.objects.prefetch_related('site', 'rack_group'), pk=pk)
powerpanel = get_object_or_404(self.queryset, pk=pk)
powerfeed_table = tables.PowerFeedTable(
data=PowerFeed.objects.filter(power_panel=powerpanel).prefetch_related('rack'),
orderable=False
@ -2529,12 +2531,12 @@ class PowerFeedListView(ObjectListView):
table = tables.PowerFeedTable
class PowerFeedView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_powerfeed'
class PowerFeedView(ObjectView):
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
def get(self, request, pk):
powerfeed = get_object_or_404(PowerFeed.objects.prefetch_related('power_panel', 'rack'), pk=pk)
powerfeed = get_object_or_404(self.queryset, pk=pk)
return render(request, 'dcim/powerfeed.html', {
'powerfeed': powerfeed,

View File

@ -13,7 +13,7 @@ from django_tables2 import RequestConfig
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.utils import shallow_compare_dict
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
from utilities.views import BulkDeleteView, BulkEditView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView
from . import filters, forms
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
from .reports import get_report, get_reports
@ -37,12 +37,12 @@ class TagListView(ObjectListView):
action_buttons = ()
class TagView(PermissionRequiredMixin, View):
permission_required = 'extras.view_tag'
class TagView(ObjectView):
queryset = Tag.objects.all()
def get(self, request, slug):
tag = get_object_or_404(Tag, slug=slug)
tag = get_object_or_404(self.queryset, slug=slug)
tagged_items = TaggedItem.objects.filter(
tag=tag
).prefetch_related(
@ -109,11 +109,11 @@ class ConfigContextListView(ObjectListView):
action_buttons = ('add',)
class ConfigContextView(PermissionRequiredMixin, View):
permission_required = 'extras.view_configcontext'
class ConfigContextView(ObjectView):
queryset = ConfigContext.objects.all()
def get(self, request, pk):
configcontext = get_object_or_404(ConfigContext, pk=pk)
configcontext = get_object_or_404(self.queryset, pk=pk)
# Determine user's preferred output format
if request.GET.get('format') in ['json', 'yaml']:
@ -195,12 +195,12 @@ class ObjectChangeListView(ObjectListView):
action_buttons = ('export',)
class ObjectChangeView(PermissionRequiredMixin, View):
permission_required = 'extras.view_objectchange'
class ObjectChangeView(ObjectView):
queryset = ObjectChange.objects.all()
def get(self, request, pk):
objectchange = get_object_or_404(ObjectChange, pk=pk)
objectchange = get_object_or_404(self.queryset, pk=pk)
related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk)
related_changes_table = ObjectChangeTable(

View File

@ -10,8 +10,8 @@ from django_tables2 import RequestConfig
from dcim.models import Device, Interface
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
ObjectPermissionRequiredMixin,
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
ObjectListView,
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
@ -120,12 +120,12 @@ class VRFListView(ObjectListView):
table = tables.VRFTable
class VRFView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vrf'
class VRFView(ObjectView):
queryset = VRF.objects.all()
def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
vrf = get_object_or_404(self.queryset, pk=pk)
prefix_count = Prefix.objects.filter(vrf=vrf).count()
return render(request, 'ipam/vrf.html', {
@ -298,12 +298,12 @@ class AggregateListView(ObjectListView):
}
class AggregateView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_aggregate'
class AggregateView(ObjectView):
queryset = Aggregate.objects.all()
def get(self, request, pk):
aggregate = get_object_or_404(Aggregate, pk=pk)
aggregate = get_object_or_404(self.queryset, pk=pk)
# Find all child prefixes contained by this aggregate
child_prefixes = Prefix.objects.filter(
@ -422,8 +422,7 @@ class PrefixListView(ObjectListView):
return self.queryset.annotate_depth(limit=limit)
class PrefixView(ObjectPermissionRequiredMixin, View):
permission_required = 'ipam.view_prefix'
class PrefixView(ObjectView):
queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
def get(self, request, pk):
@ -465,12 +464,12 @@ class PrefixView(ObjectPermissionRequiredMixin, View):
})
class PrefixPrefixesView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_prefix'
class PrefixPrefixesView(ObjectView):
queryset = Prefix.objects.all()
def get(self, request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
prefix = get_object_or_404(self.queryset, pk=pk)
# Child prefixes table
child_prefixes = prefix.get_child_prefixes().prefetch_related(
@ -509,12 +508,12 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
})
class PrefixIPAddressesView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_prefix'
class PrefixIPAddressesView(ObjectView):
queryset = Prefix.objects.all()
def get(self, request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
prefix = get_object_or_404(self.queryset, pk=pk)
# Find all IPAddresses belonging to this Prefix
ipaddresses = prefix.get_child_ips().prefetch_related(
@ -601,12 +600,12 @@ class IPAddressListView(ObjectListView):
table = tables.IPAddressDetailTable
class IPAddressView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_ipaddress'
class IPAddressView(ObjectView):
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
def get(self, request, pk):
ipaddress = get_object_or_404(IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), pk=pk)
ipaddress = get_object_or_404(self.queryset, pk=pk)
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(
@ -833,14 +832,12 @@ class VLANListView(ObjectListView):
table = tables.VLANDetailTable
class VLANView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vlan'
class VLANView(ObjectView):
queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role')
def get(self, request, pk):
vlan = get_object_or_404(VLAN.objects.prefetch_related(
'site__region', 'tenant__group', 'role'
), pk=pk)
vlan = get_object_or_404(self.queryset, pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).prefetch_related('vrf', 'site', 'role')
prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
prefix_table.exclude = ('vlan',)
@ -851,12 +848,12 @@ class VLANView(PermissionRequiredMixin, View):
})
class VLANMembersView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vlan'
class VLANMembersView(ObjectView):
queryset = VLAN.objects.all()
def get(self, request, pk):
vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
vlan = get_object_or_404(self.queryset, pk=pk)
members = vlan.get_members().prefetch_related('device', 'virtual_machine')
members_table = tables.VLANMemberTable(members)
@ -920,12 +917,12 @@ class ServiceListView(ObjectListView):
action_buttons = ('export',)
class ServiceView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_service'
class ServiceView(ObjectView):
queryset = Service.objects.all()
def get(self, request, pk):
service = get_object_or_404(Service, pk=pk)
service = get_object_or_404(self.queryset, pk=pk)
return render(request, 'ipam/service.html', {
'service': service,

View File

@ -9,7 +9,8 @@ from django.urls import reverse
from django.views.generic import View
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, GetReturnURLMixin, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkDeleteView, BulkEditView, BulkImportView, GetReturnURLMixin, ObjectView, ObjectDeleteView, ObjectEditView,
ObjectListView,
)
from . import filters, forms, tables
from .decorators import userkey_required
@ -66,12 +67,12 @@ class SecretListView(ObjectListView):
action_buttons = ('import', 'export')
class SecretView(PermissionRequiredMixin, View):
permission_required = 'secrets.view_secret'
class SecretView(ObjectView):
queryset = Secret.objects.all()
def get(self, request, pk):
secret = get_object_or_404(Secret, pk=pk)
secret = get_object_or_404(self.queryset, pk=pk)
return render(request, 'secrets/secret.html', {
'secret': secret,

View File

@ -1,13 +1,11 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from django.views.generic import View
from circuits.models import Circuit
from dcim.models import Site, Rack, Device, RackReservation
from ipam.models import IPAddress, Prefix, VLAN, VRF
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from virtualization.models import VirtualMachine, Cluster
from . import filters, forms, tables
@ -59,12 +57,12 @@ class TenantListView(ObjectListView):
table = tables.TenantTable
class TenantView(PermissionRequiredMixin, View):
permission_required = 'tenancy.view_tenant'
class TenantView(ObjectView):
queryset = Tenant.objects.prefetch_related('group')
def get(self, request, slug):
tenant = get_object_or_404(Tenant, slug=slug)
tenant = get_object_or_404(self.queryset, slug=slug)
stats = {
'site_count': Site.objects.filter(tenant=tenant).count(),
'rack_count': Rack.objects.filter(tenant=tenant).count(),

View File

@ -118,6 +118,18 @@ class GetReturnURLMixin(object):
# Generic views
#
class ObjectView(ObjectPermissionRequiredMixin, View):
"""
Retrieve a single object for display.
:param queryset: The base queryset for retrieving the object.
"""
queryset = None
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view')
class ObjectListView(ObjectPermissionRequiredMixin, View):
"""
List a series of objects.

View File

@ -11,8 +11,8 @@ from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
from ipam.models import Service
from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectDeleteView,
ObjectEditView, ObjectListView,
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectView,
ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -85,12 +85,12 @@ class ClusterListView(ObjectListView):
filterset_form = forms.ClusterFilterForm
class ClusterView(PermissionRequiredMixin, View):
permission_required = 'virtualization.view_cluster'
class ClusterView(ObjectView):
queryset = Cluster.objects.all()
def get(self, request, pk):
cluster = get_object_or_404(Cluster, pk=pk)
cluster = get_object_or_404(self.queryset, pk=pk)
devices = Device.objects.filter(cluster=cluster).prefetch_related(
'site', 'rack', 'tenant', 'device_type__manufacturer'
)
@ -233,12 +233,12 @@ class VirtualMachineListView(ObjectListView):
template_name = 'virtualization/virtualmachine_list.html'
class VirtualMachineView(PermissionRequiredMixin, View):
permission_required = 'virtualization.view_virtualmachine'
class VirtualMachineView(ObjectView):
queryset = VirtualMachine.objects.prefetch_related('tenant__group')
def get(self, request, pk):
virtualmachine = get_object_or_404(VirtualMachine.objects.prefetch_related('tenant__group'), pk=pk)
virtualmachine = get_object_or_404(self.queryset, pk=pk)
interfaces = Interface.objects.filter(virtual_machine=virtualmachine)
services = Service.objects.filter(virtual_machine=virtualmachine)