feat(users): Add support for cloning ObjectPermission objects

Introduces cloning functionality for ObjectPermission objects using the
CloningMixin. Updates the constraints field handling, adds JSONField,
and introduces logic to process initial data for cloned objects.

Fixes #15492
This commit is contained in:
Martin Hauser
2025-09-14 17:07:02 +02:00
committed by Jeremy Stretch
parent 684106031a
commit 34b111bdc4
2 changed files with 40 additions and 13 deletions

View File

@@ -1,3 +1,5 @@
import json
from django import forms
from django.conf import settings
from django.contrib.auth import password_validation
@@ -13,7 +15,11 @@ from netbox.preferences import PREFERENCES
from users.constants import *
from users.models import *
from utilities.data import flatten_dict
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.fields import (
ContentTypeMultipleChoiceField,
DynamicModelMultipleChoiceField,
JSONField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
from utilities.permissions import qs_filter_from_constraints
@@ -316,13 +322,22 @@ class ObjectPermissionForm(forms.ModelForm):
required=False,
queryset=Group.objects.all()
)
constraints = JSONField(
required=False,
label=_('Constraints'),
help_text=_(
'JSON expression of a queryset filter that will return only permitted objects. Leave null '
'to match all objects of this type. A list of multiple objects will result in a logical OR '
'operation.'
),
)
fieldsets = (
FieldSet('name', 'description', 'enabled'),
FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')),
FieldSet('object_types', name=_('Objects')),
FieldSet('groups', 'users', name=_('Assignment')),
FieldSet('constraints', name=_('Constraints'))
FieldSet('constraints', name=_('Constraints')),
)
class Meta:
@@ -330,13 +345,6 @@ class ObjectPermissionForm(forms.ModelForm):
fields = [
'name', 'description', 'enabled', 'object_types', 'users', 'groups', 'constraints', 'actions',
]
help_texts = {
'constraints': _(
'JSON expression of a queryset filter that will return only permitted objects. Leave null '
'to match all objects of this type. A list of multiple objects will result in a logical OR '
'operation.'
)
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -344,18 +352,32 @@ class ObjectPermissionForm(forms.ModelForm):
# Make the actions field optional since the form uses it only for non-CRUD actions
self.fields['actions'].required = False
# Populate assigned users and groups
# Prepare the appropriate fields when editing an existing ObjectPermission
if self.instance.pk:
# Populate assigned users and groups
self.fields['groups'].initial = self.instance.groups.values_list('id', flat=True)
self.fields['users'].initial = self.instance.users.values_list('id', flat=True)
# Check the appropriate checkboxes when editing an existing ObjectPermission
if self.instance.pk:
# Check the appropriate checkboxes when editing an existing ObjectPermission
for action in ['view', 'add', 'change', 'delete']:
if action in self.instance.actions:
self.fields[f'can_{action}'].initial = True
self.instance.actions.remove(action)
# Populate initial data for a new ObjectPermission
elif self.initial:
# Handle cloned objects - actions come from initial data (URL parameters)
if 'actions' in self.initial:
if cloned_actions := self.initial['actions']:
for action in ['view', 'add', 'change', 'delete']:
if action in cloned_actions:
self.fields[f'can_{action}'].initial = True
self.initial['actions'].remove(action)
# Convert data delivered via initial data to JSON data
if 'constraints' in self.initial:
if type(self.initial['constraints']) is str:
self.initial['constraints'] = json.loads(self.initial['constraints'])
def clean(self):
super().clean()

View File

@@ -3,6 +3,7 @@ from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models.features import CloningMixin
from utilities.querysets import RestrictedQuerySet
__all__ = (
@@ -10,7 +11,7 @@ __all__ = (
)
class ObjectPermission(models.Model):
class ObjectPermission(CloningMixin, models.Model):
"""
A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects
identified by ORM query parameters.
@@ -43,6 +44,10 @@ class ObjectPermission(models.Model):
help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
)
clone_fields = (
'description', 'enabled', 'object_types', 'actions', 'constraints',
)
objects = RestrictedQuerySet.as_manager()
class Meta: