Просмотр исходного кода

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
Jeremy Stretch 2 лет назад
Родитель
Сommit
450790ab4a

+ 7 - 2
netbox/core/views.py

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

+ 61 - 84
netbox/dcim/views.py

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

+ 27 - 6
netbox/extras/views.py

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

+ 9 - 0
netbox/netbox/constants.py

@@ -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'},
+}

+ 48 - 13
netbox/netbox/views/generic/mixins.py

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

+ 5 - 1
netbox/tenancy/views.py

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

+ 8 - 11
netbox/virtualization/views.py

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