Skip to content

Provide API for Registering Custom Model Actions. #21357

@jnovinger

Description

@jnovinger

NetBox version

v4.5.2

Feature type

New functionality

Proposed functionality

This FR proposes adding a registration API for custom permission actions on models. Currently, plugins like netbox-branching define custom actions (e.g., merge, sync, revert) that require the "Additional actions" freeform text field in ObjectPermission configuration. This approach is error-prone and not discoverable.

The Problem

To allow non-superusers to perform custom actions like merging branches, administrators must:

  1. Know the exact action name string (case-sensitive: "merge" not "Merge")
  2. Navigate to Admin > Authentication > Permissions
  3. Type the action into a freeform text field with no validation or autocomplete
  4. Hope they didn't make a typo

This is counterintuitive, users expect checkbox-based permissions like the standard view/add/change/delete. The "Additional actions" field provides no indication of what valid actions exist for the selected models.

Proposed Solution

1. Add register_model_actions() following the pattern of existing shared registration APIs

Following the precedent of register_search, register_model_view, and register_filterset (which are used by both core apps and plugins), add a new registration function:

from netbox.registry import register_model_actions
from netbox.models import ModelAction

@register_model_actions
class BranchActions:
    """Custom permission actions for the Branch model"""
    model = Branch
    actions = [
        ModelAction('sync', help_text="Synchronize branch with main schema"),
        ModelAction('merge', help_text="Merge branch changes into main"),
        ModelAction('revert'),  # No help text
        ModelAction('archive'),
        ModelAction('migrate'),
    ]

or

register_model_actions(Branch, [
    ModelAction('sync', help_text="Synchronize branch with main schema"),
    ModelAction('merge', help_text="Merge branch changes into main"),
    ModelAction('revert'),  # No help text
    ModelAction('archive'),
    ModelAction('migrate'),
])

The ModelAction class follows the pattern of MenuItem, MenuGroup, and other similar dataclasses in core:

@dataclass
class ModelAction:
    """Represents a custom permission action for a model"""
    name: str
    help_text: Optional[str] = None

Note: The exact API (decorator vs function call) should match whichever pattern is most consistent with existing registration APIs. The key elements are:

  • A ModelAction class for structured action definitions with optional help text
  • A registration mechanism that stores actions in the registry
  • Support for use by both core apps and plugins

2. Modify ObjectPermissionForm to render registered actions as checkboxes

The form renders all registered custom actions, organized by application and model:

Actions
├── ☑ View
├── ☑ Add
├── ☑ Change
├── ☐ Delete

Additional actions  [________________]  (freeform field retained)

NetBox Branching
└── Branch
    ├── ☐ Sync         "Synchronize branch with main schema"
    ├── ☐ Merge        "Merge branch changes into main"
    ├── ☐ Revert
    ├── ☐ Archive
    └── ☐ Migrate

NetBox Changes
└── Policy
    └── ☐ Bypass       "Override protect_main restriction..."

The standard CRUD checkboxes (view/add/change/delete) remain at the top as they apply universally. Custom action sections are organized by app label and model name. Help text (when provided) is displayed alongside checkboxes.

3. Implement show/hide behavior based on selected object_types

When a user changes the selected object types, JavaScript shows only the relevant action sections. For example:

  • Select only "Branch" → show only the NetBox Branching > Branch section
  • Select "Branch" and "Policy" → show both sections (union)
  • Select no types → show no custom action sections

The full set of registered actions is rendered server-side in the initial page load. Since the set of models and their registered actions is static at runtime (only changes on plugin install/upgrade or restart), no AJAX calls are needed. The JavaScript simply toggles visibility based on the pre-rendered data.

This approach is acceptable because ObjectPermission editing is a low-traffic administrative page. The additional markup is negligible.

4. Retain the freeform "Additional actions" field

For backwards compatibility and edge cases, the existing freeform text field is retained. Users can still type custom actions manually if needed.

5. Add backend validation

Extend ObjectPermissionForm.clean() to validate that selected actions are valid for the selected object types:

def clean(self):
    # ... existing validation ...

    # Build set of valid custom actions for selected types (union)
    # Validate that all selected actions are either CRUD or registered for selected types
    # Raise ValidationError for invalid action/object_type combinations

This catches:

  • Typos in the freeform field
  • Form data manipulation
  • Invalid action/object_type combinations that slip through the UI

Design Considerations

Why a ModelAction class rather than tuples or dicts?

Following the pattern of PluginMenuItem and PluginMenuButton, a dedicated class provides:

  • Clear, self-documenting API
  • Optional parameters without awkward None values in tuples
  • Extensibility for future attributes
  • Consistency with existing NetBox patterns

Why a shared registration API rather than a Plugin-only API?

NetBox core already has custom actions (e.g., sync on DataSource) that suffer from the same discoverability problem. Following the precedent of register_search, register_model_view, and register_filterset (which are all used by both core apps and plugins) this API should be usable by both.

Why render all actions server-side rather than fetch dynamically?

The set of models and their registered actions is essentially static at runtime. Rendering everything upfront and using JavaScript to show/hide is simpler than implementing AJAX endpoints, and performs well since this is a low-traffic admin page.

Use case

This benefits any code, in core NetBox or any plugin, that defines custom permission actions beyond the standard view/add/change/delete:

Core NetBox:

  • DataSource model has sync action
  • Device model has render_config action
  • VirtualMachine model has render_config action

Plugins:

  • netbox-branching: Branch model has sync, merge, revert, archive, migrate actions
  • netbox-changes: Policy model has bypass action for overriding protect_main
  • Future plugins with custom model actions gain immediate discoverability

Administrators can configure permissions correctly without consulting documentation to discover valid action strings.

Database changes

None

External dependencies

None

Metadata

Metadata

Assignees

No one assigned

    Labels

    complexity: mediumRequires a substantial but not unusual amount of effort to implementnetboxstatus: backlogAwaiting selection for worktype: featureIntroduction of new functionality to the application

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions