* Add ModelAction and register_model_actions() API for custom permission actions * Add ObjectTypeSplitMultiSelectWidget and RegisteredActionsWidget * Integrate registered actions into ObjectPermissionForm * Add JavaScript for registered actions show/hide * Register custom actions for DataSource, Device, and VirtualMachine * Add tests for ModelAction and register_model_actions * Refine registered actions widget UI - Use verbose labels (App | Model) for action group headers - Simplify template layout with h5 headers instead of cards - Consolidate Standard/Custom/Additional Actions into single Actions fieldset * Hide custom actions field when no applicable models selected The entire field row is now hidden when no selected object types have registered custom actions, avoiding an empty "Custom actions" label. * Add documentation for custom model actions - Add plugin development guide for registering custom actions - Update admin permissions docs to mention custom actions UI - Add docstrings to ModelAction and register_model_actions * Add RESERVED_ACTIONS constant and fix dedup in registered actions - Define RESERVED_ACTIONS in users/constants.py for the four built-in permission actions (view, add, change, delete) - Replace hardcoded action lists in ObjectPermissionForm with the constant - Fix duplicate action names in clean() when the same action is registered across multiple models (e.g. render_config for Device and VirtualMachine) - Fix template substring matching bug in objectpermission.html detail view by passing RESERVED_ACTIONS through view context for proper list membership * Fix shared action pre-selection and additional actions leakage on edit * Prevent duplicate action registration in register_model_actions() * Remove stale comment in RegisteredActionsWidget * Rebuild frontend assets after rebase onto feature * Refactor SplitMultiSelectWidget to use class attributes for widget classes * Reject reserved action names in register_model_actions() * Show all registered actions with enable/disable instead of show/hide * Validate action name is not empty and clarify RESERVED_ACTIONS origin * Adapt custom actions panel for declarative layout system Convert the ObjectPermission detail view to use the new panel-based layout from #21568. Add ObjectPermissionCustomActionsPanel that cross-references assigned object types with the model_actions registry to display which models each custom action applies to. Also fix dark-mode visibility of disabled action checkboxes in the permission form by overriding Bootstrap's disabled opacity. * Flatten registered actions UI and declare via Meta.permissions Implement two changes requested in review of #21560: 1. Use Meta.permissions for action declaration - Add Meta.permissions to DataSource, Device, and VirtualMachine - register_models() auto-registers actions from Meta.permissions - Remove explicit register_model_actions() calls from apps.py - Add get_action_model_map() utility to utilities/permissions.py 2. Flatten the ObjectPermission form UI - Show a single deduplicated list of action checkboxes (one per unique action name) instead of grouped-by-model checkboxes - RegisteredActionsWidget uses create_option() to inject model_keys and help_text; JS enables/disables based on selected object types - render_field.html bypasses outer wrapper for registeredactionswidget so widget emits rows with identical DOM structure to CRUD checkboxes - Unchecking a model now also unchecks unsupported action checkboxes Fixes #21357 * Address review feedback on registered actions - Sort model_keys in data-models attribute for deterministic output - Rename registered_actions field label to 'Registered actions' - Target object_types selected list via data-object-types-selected attribute instead of hardcoded DOM ID - Reduce setTimeout delay to 0ms since moveOption() is synchronous * Consolidate ObjectPermission detail view actions panel Merge ObjectPermissionActionsPanel and ObjectPermissionCustomActionsPanel into a single Actions panel that shows CRUD booleans and all registered actions in one table, matching the form's consolidated layout. Also fix data-object-types-selected attribute value (True -> 'true') and update plugin docs to show Meta.permissions as the primary registration approach. * Address additional bot review feedback - clean() collects all validation errors before raising instead of stopping at the first - Fix stale admin docs (still referenced "Custom actions" and "grouped by model") * Update netbox/netbox/registry.py Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com> * Fix model_actions registry to use set operations The registry was changed to defaultdict(set) but the registration code still used list methods. Update .append() to .add() and fix tests to use set-compatible access patterns. * Rename permission migrations for clarity * Move ModelAction validation into __post_init__ * Drop model name from permission descriptions * Simplify ObjectPermission form and remove custom widgets Replace the dynamic UI with standard BooleanField checkboxes for each registered action. No custom widgets, no JavaScript, no template changes. - Remove RegisteredActionsWidget, ObjectTypeSplitMultiSelectWidget, and registeredActions.ts - Use dynamic BooleanFields for registered actions (renders identically to CRUD checkboxes) - Move action-resolution logic from panel to ObjectPermission model - Remove object-type cross-validation from form clean() - Remove unused get_action_model_map utility * Remove register_model_actions from public API Meta.permissions is the documented approach for plugins. The register_model_actions function is now an internal implementation detail. * Sort registered actions and improve test coverage Sort action names alphabetically for stable display order. Add tests for cloning, empty registry, and models_csv output. * Add help_text to registered action checkboxes * Return model_keys as list from get_registered_actions() Move string joining to the template so callers get native list data instead of a pre-formatted CSV string. * Improve detail view: human-friendly descriptions and additional actions Return dicts from get_registered_actions() with help_text and verbose model names. Add get_additional_actions() for manually-entered actions that aren't CRUD or registered. Show both in the Actions panel. * Renumber permission migrations after feature merge Resolve migration conflicts with default_ordering_indexes migrations. Renumber to 0023 (core), 0232 (dcim), 0056 (virtualization) and update dependencies. --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
7.1 KiB
Object-Based Permissions
NetBox employs a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
A permission in NetBox represents a relationship shared by several components:
- Object type(s) - One or more types of object in NetBox
- User(s)/Group(s) - One or more users or groups of users
- Action(s) - The action(s) that can be performed on an object
- Constraints - An arbitrary filter used to limit the granted action(s) to a specific subset of objects
At a minimum, a permission assignment must specify one object type, one user or group, and one action. The specification of constraints is optional: A permission without any constraints specified will apply to all instances of the selected model(s).
Actions
There are four core actions that can be permitted for each type of object within NetBox, roughly analogous to the CRUD convention (create, read, update, and delete):
- View - Retrieve an object from the database
- Add - Create a new object
- Change - Modify an existing object
- Delete - Delete an existing object
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the sync action for data sources allows a user to synchronize data from a remote source, and the render_config action for devices and virtual machines allows rendering configuration templates.
Some models have registered actions that appear as checkboxes in the "Actions" section when creating or editing a permission. These are shown in a flat list alongside the built-in CRUD actions. Additional actions (such as those not yet registered by a plugin, or for backwards compatibility) can be entered manually in the "Additional actions" field.
!!! note
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named actions.
Constraints
Constraints are expressed as a JSON object or list representing a Django query filter. This is the same syntax that you would pass to the QuerySet filter() method when performing a query using the Django ORM. As with query filters, double underscores can be used to traverse related objects or invoke lookup expressions. Some example queries and their corresponding definitions are shown below.
All attributes defined within a single JSON object are applied with a logical AND. For example, suppose you assign a permission for the site model with the following constraints.
{
"status": "active",
"region__name": "Americas"
}
The permission will grant access only to sites which have a status of "active" and which are assigned to the "Americas" region.
To achieve a logical OR with a different set of constraints, define multiple objects within a list. For example, if you want to constrain the permission to VLANs with an ID between 100 and 199 or a status of "reserved," do the following:
[
{
"vid__gte": 100,
"vid__lt": 200
},
{
"status": "reserved"
}
]
Additionally, where multiple permissions have been assigned for an object type, their collective constraints will be merged using a logical "OR" operation.
User Token
When defining a permission constraint, administrators may use the special token $user to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as:
{
"created_by": "$user"
}
The $user token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes.
Default Permissions
While permissions are typically assigned to specific groups and/or users, it is also possible to define a set of default permissions that are applied to all authenticated users. This is done using the DEFAULT_PERMISSIONS configuration parameter. Note that statically configuring permissions for specific users or groups is not supported.
Example Constraint Definitions
| Constraints | Description |
|---|---|
{"status": "active"} |
Status is active |
{"status__in": ["planned", "reserved"]} |
Status is active OR reserved |
{"status": "active", "role": "testing"} |
Status is active AND role is testing |
{"name__startswith": "Foo"} |
Name starts with "Foo" (case-sensitive) |
{"name__iendswith": "bar"} |
Name ends with "bar" (case-insensitive) |
{"vid__gte": 100, "vid__lt": 200} |
VLAN ID is greater than or equal to 100 AND less than 200 |
[{"vid__lt": 200}, {"status": "reserved"}] |
VLAN ID is less than 200 OR status is reserved |
Permissions Enforcement
Viewing Objects
Object-based permissions work by filtering the database query generated by a user's request to restrict the set of objects returned. When a request is received, NetBox first determines whether the user is authenticated and has been granted permission to perform the requested action. For example, if the requested URL is /dcim/devices/, NetBox will check for the dcim.view_device permission. If the user has not been assigned this permission (either directly or via a group assignment), NetBox will return a 403 (forbidden) HTTP response.
If the permission has been granted, NetBox will compile any specified constraints for the model and action. For example, suppose two permissions have been assigned to the user granting view access to the device model, with the following constraints:
[
{"site__name__in": ["NYC1", "NYC2"]},
{"status": "offline", "tenant__isnull": true}
]
This grants the user access to view any device that is assigned to a site named NYC1 or NYC2, or which has a status of "offline" and has no tenant assigned. These constraints are equivalent to the following ORM query:
Device.objects.filter(
Q(site__name__in=['NYC1', 'NYC2']),
Q(status='offline', tenant__isnull=True)
)
Creating and Modifying Objects
The same sort of logic is in play when a user attempts to create or modify an object in NetBox, with a twist. Once validation has completed, NetBox starts an atomic database transaction to facilitate the change, and the object is created or saved normally. Next, still within the transaction, NetBox issues a second query to retrieve the newly created/updated object, filtering the restricted queryset with the object's primary key. If this query fails to return the object, NetBox knows that the new revision does not match the constraints imposed by the permission. The transaction is then rolled back, leaving the database in its original state prior to the change, and the user is informed of the violation.