Closes #13550: Refactor view action mappings (#14062)

* Merge actions and action_perms into a single mapping

* Update obsolete permission maps

* Update obsolete action lists

* Normalize empty permission mappings

* Cleanup

* Add deprecation warnings

* Introduce DEFAULT_ACTION_PERMISSIONS constant
This commit is contained in:
Jeremy Stretch 2023-10-20 15:08:09 -04:00 committed by GitHub
parent 3f40ee5501
commit 450790ab4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 117 deletions

View File

@ -100,7 +100,9 @@ class DataFileListView(generic.ObjectListView):
filterset = filtersets.DataFileFilterSet
filterset_form = forms.DataFileFilterForm
table = tables.DataFileTable
actions = ('bulk_delete',)
actions = {
'bulk_delete': {'delete'},
}
@register_model_view(DataFile)
@ -128,7 +130,10 @@ class JobListView(generic.ObjectListView):
filterset = filtersets.JobFilterSet
filterset_form = forms.JobFilterForm
table = tables.JobTable
actions = ('export', 'delete', 'bulk_delete')
actions = {
'export': {'view'},
'bulk_delete': {'delete'},
}
class JobView(generic.ObjectView):

View File

@ -20,6 +20,7 @@ from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
from ipam.tables import InterfaceVLANTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm
@ -46,15 +47,11 @@ CABLE_TERMINATION_TYPES = {
class DeviceComponentsView(generic.ObjectChildrenView):
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename', 'bulk_disconnect')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
'bulk_disconnect': {'change'},
})
}
queryset = Device.objects.all()
def get_children(self, request, parent):
@ -1977,7 +1974,10 @@ class DeviceModuleBaysView(DeviceComponentsView):
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
template_name = 'dcim/device/modulebays.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.module_bay_count,
@ -1993,7 +1993,10 @@ class DeviceDeviceBaysView(DeviceComponentsView):
table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet
template_name = 'dcim/device/devicebays.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.device_bay_count,
@ -2005,11 +2008,14 @@ class DeviceDeviceBaysView(DeviceComponentsView):
@register_model_view(Device, 'inventory')
class DeviceInventoryView(DeviceComponentsView):
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
child_model = InventoryItem
table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet
template_name = 'dcim/device/inventory.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_count,
@ -2187,14 +2193,10 @@ class ConsolePortListView(generic.ObjectListView):
filterset_form = forms.ConsolePortFilterForm
table = tables.ConsolePortTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(ConsolePort)
@ -2259,14 +2261,10 @@ class ConsoleServerPortListView(generic.ObjectListView):
filterset_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(ConsoleServerPort)
@ -2331,14 +2329,10 @@ class PowerPortListView(generic.ObjectListView):
filterset_form = forms.PowerPortFilterForm
table = tables.PowerPortTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(PowerPort)
@ -2403,14 +2397,10 @@ class PowerOutletListView(generic.ObjectListView):
filterset_form = forms.PowerOutletFilterForm
table = tables.PowerOutletTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(PowerOutlet)
@ -2475,14 +2465,10 @@ class InterfaceListView(generic.ObjectListView):
filterset_form = forms.InterfaceFilterForm
table = tables.InterfaceTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(Interface)
@ -2595,14 +2581,10 @@ class FrontPortListView(generic.ObjectListView):
filterset_form = forms.FrontPortFilterForm
table = tables.FrontPortTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(FrontPort)
@ -2667,14 +2649,10 @@ class RearPortListView(generic.ObjectListView):
filterset_form = forms.RearPortFilterForm
table = tables.RearPortTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(RearPort)
@ -2739,14 +2717,10 @@ class ModuleBayListView(generic.ObjectListView):
filterset_form = forms.ModuleBayFilterForm
table = tables.ModuleBayTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(ModuleBay)
@ -2803,14 +2777,10 @@ class DeviceBayListView(generic.ObjectListView):
filterset_form = forms.DeviceBayFilterForm
table = tables.DeviceBayTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(DeviceBay)
@ -2936,14 +2906,10 @@ class InventoryItemListView(generic.ObjectListView):
filterset_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable
template_name = 'dcim/component_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
})
}
@register_model_view(InventoryItem)
@ -3175,7 +3141,12 @@ class CableListView(generic.ObjectListView):
filterset = filtersets.CableFilterSet
filterset_form = forms.CableFilterForm
table = tables.CableTable
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
actions = {
'import': {'add'},
'export': {'view'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(Cable)
@ -3269,7 +3240,9 @@ class ConsoleConnectionsListView(generic.ObjectListView):
filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable
template_name = 'dcim/connections_list.html'
actions = ('export',)
actions = {
'export': {'view'},
}
def get_extra_context(self, request):
return {
@ -3283,7 +3256,9 @@ class PowerConnectionsListView(generic.ObjectListView):
filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable
template_name = 'dcim/connections_list.html'
actions = ('export',)
actions = {
'export': {'view'},
}
def get_extra_context(self, request):
return {
@ -3297,7 +3272,9 @@ class InterfaceConnectionsListView(generic.ObjectListView):
filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable
template_name = 'dcim/connections_list.html'
actions = ('export',)
actions = {
'export': {'view'},
}
def get_extra_context(self, request):
return {

View File

@ -16,6 +16,7 @@ from core.tables import JobTable
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class
from netbox.config import get_config, PARAMS
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from utilities.forms import ConfirmationForm, get_field_value
from utilities.htmx import is_htmx
@ -210,7 +211,10 @@ class ExportTemplateListView(generic.ObjectListView):
filterset_form = forms.ExportTemplateFilterForm
table = tables.ExportTemplateTable
template_name = 'extras/exporttemplate_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_sync')
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_sync': {'sync'},
}
@register_model_view(ExportTemplate)
@ -472,7 +476,12 @@ class ConfigContextListView(generic.ObjectListView):
filterset_form = forms.ConfigContextFilterForm
table = tables.ConfigContextTable
template_name = 'extras/configcontext_list.html'
actions = ('add', 'bulk_edit', 'bulk_delete', 'bulk_sync')
actions = {
'add': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
'bulk_sync': {'sync'},
}
@register_model_view(ConfigContext)
@ -576,7 +585,10 @@ class ConfigTemplateListView(generic.ObjectListView):
filterset_form = forms.ConfigTemplateFilterForm
table = tables.ConfigTemplateTable
template_name = 'extras/configtemplate_list.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_sync')
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_sync': {'sync'},
}
@register_model_view(ConfigTemplate)
@ -627,7 +639,9 @@ class ObjectChangeListView(generic.ObjectListView):
filterset_form = forms.ObjectChangeFilterForm
table = tables.ObjectChangeTable
template_name = 'extras/objectchange_list.html'
actions = ('export',)
actions = {
'export': {'view'},
}
@register_model_view(ObjectChange)
@ -693,7 +707,9 @@ class ImageAttachmentListView(generic.ObjectListView):
filterset = filtersets.ImageAttachmentFilterSet
filterset_form = forms.ImageAttachmentFilterForm
table = tables.ImageAttachmentTable
actions = ('export',)
actions = {
'export': {'view'},
}
@register_model_view(ImageAttachment, 'edit')
@ -736,7 +752,12 @@ class JournalEntryListView(generic.ObjectListView):
filterset = filtersets.JournalEntryFilterSet
filterset_form = forms.JournalEntryFilterForm
table = tables.JournalEntryTable
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
actions = {
'import': {'add'},
'export': {'view'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(JournalEntry)

View File

@ -27,3 +27,12 @@ ADVISORY_LOCK_KEYS = {
'inventoryitem': 105700,
'inventoryitemtemplate': 105800,
}
# Default view action permission mapping
DEFAULT_ACTION_PERMISSIONS = {
'add': {'add'},
'import': {'add'},
'export': {'view'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}

View File

@ -1,5 +1,6 @@
from collections import defaultdict
import warnings
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from utilities.permissions import get_permission_for_model
__all__ = (
@ -9,13 +10,15 @@ __all__ = (
class ActionsMixin:
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
})
"""
Maps action names to the set of required permissions for each. Object list views reference this mapping to
determine whether to render the applicable button for each action: The button will be rendered only if the user
possesses the specified permission(s).
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
with custom actions, such as bulk_sync.
"""
actions = DEFAULT_ACTION_PERMISSIONS
def get_permitted_actions(self, user, model=None):
"""
@ -23,11 +26,43 @@ class ActionsMixin:
"""
model = model or self.queryset.model
return [
action for action in self.actions if user.has_perms([
get_permission_for_model(model, name) for name in self.action_perms[action]
])
]
# TODO: Remove backward compatibility in Netbox v4.0
# Determine how permissions are being mapped to actions for the view
if hasattr(self, 'action_perms'):
# Backward compatibility for <3.7
permissions_map = self.action_perms
warnings.warn(
"Setting action_perms on views is deprecated and will be removed in NetBox v4.0. Use actions instead.",
DeprecationWarning
)
elif type(self.actions) is dict:
# New actions format (3.7+)
permissions_map = self.actions
else:
# actions is still defined as a list or tuple (<3.7) but no custom mapping is defined; use the old
# default mapping
permissions_map = {
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
warnings.warn(
"View actions should be defined as a dictionary mapping. Support for the legacy list format will be "
"removed in NetBox v4.0.",
DeprecationWarning
)
# Resolve required permissions for each action
permitted_actions = []
for action in self.actions:
required_permissions = [
get_permission_for_model(model, name) for name in permissions_map.get(action, set())
]
if not required_permissions or user.has_perms(required_permissions):
permitted_actions.append(action)
return permitted_actions
class TableMixin:

View File

@ -386,7 +386,11 @@ class ContactAssignmentListView(generic.ObjectListView):
filterset = filtersets.ContactAssignmentFilterSet
filterset_form = forms.ContactAssignmentFilterForm
table = tables.ContactAssignmentTable
actions = ('export', 'bulk_edit', 'bulk_delete')
actions = {
'export': {'view'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(ContactAssignment, 'edit')

View File

@ -16,6 +16,7 @@ from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
from ipam.models import IPAddress
from ipam.tables import InterfaceVLANTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.utils import count_related
@ -199,13 +200,13 @@ class ClusterDevicesView(generic.ObjectChildrenView):
table = DeviceTable
filterset = DeviceFilterSet
template_name = 'virtualization/cluster/devices.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_remove_devices')
action_perms = defaultdict(set, **{
actions = {
'add': {'add'},
'import': {'add'},
'export': {'view'},
'bulk_edit': {'change'},
'bulk_remove_devices': {'change'},
})
}
tab = ViewTab(
label=_('Devices'),
badge=lambda obj: obj.devices.count(),
@ -359,20 +360,16 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
table = tables.VirtualMachineVMInterfaceTable
filterset = filtersets.VMInterfaceFilterSet
template_name = 'virtualization/virtualmachine/interfaces.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interface_count,
permission='virtualization.view_vminterface',
weight=500
)
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
'bulk_rename': {'change'},
})
def get_children(self, request, parent):
return parent.interfaces.restrict(request.user, 'view').prefetch_related(