Skip to content

Commit

Permalink
Closes #15464: Move permission assignments to user & group models (#1…
Browse files Browse the repository at this point in the history
…5554)

* Move user & group M2M assignments for ObjectPermission

* Restore users & groups fields on ObjectPermission serializer
  • Loading branch information
jeremystretch committed Mar 29, 2024
1 parent 8767577 commit c8d9d93
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 87 deletions.
3 changes: 2 additions & 1 deletion netbox/netbox/api/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ def validate(self, data):
attrs.pop('custom_fields', None)

# Skip ManyToManyFields
opts = self.Meta.model._meta
m2m_values = {}
for field in self.Meta.model._meta.local_many_to_many:
for field in [*opts.local_many_to_many, *opts.related_objects]:
if field.name in attrs:
m2m_values[field.name] = attrs.pop(field.name)

Expand Down
17 changes: 8 additions & 9 deletions netbox/users/api/serializers_/permissions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers

from core.models import ObjectType
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import ValidatedModelSerializer
from users.models import Group, ObjectPermission
from .users import GroupSerializer, UserSerializer
from users.api.nested_serializers import NestedGroupSerializer, NestedUserSerializer
from users.models import Group, ObjectPermission, User

__all__ = (
'ObjectPermissionSerializer',
Expand All @@ -20,14 +19,14 @@ class ObjectPermissionSerializer(ValidatedModelSerializer):
)
groups = SerializedPKRelatedField(
queryset=Group.objects.all(),
serializer=GroupSerializer,
serializer=NestedGroupSerializer,
nested=True,
required=False,
many=True
)
users = SerializedPKRelatedField(
queryset=get_user_model().objects.all(),
serializer=UserSerializer,
queryset=User.objects.all(),
serializer=NestedUserSerializer,
nested=True,
required=False,
many=True
Expand All @@ -36,9 +35,9 @@ class ObjectPermissionSerializer(ValidatedModelSerializer):
class Meta:
model = ObjectPermission
fields = (
'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions',
'constraints',
'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'actions', 'constraints',
'groups', 'users',
)
brief_fields = (
'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions',
'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'actions',
)
23 changes: 20 additions & 3 deletions netbox/users/api/serializers_/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

from netbox.api.fields import SerializedPKRelatedField
from netbox.api.serializers import ValidatedModelSerializer
from users.models import Group
from users.models import Group, ObjectPermission
from .permissions import ObjectPermissionSerializer

__all__ = (
'GroupSerializer',
Expand All @@ -16,10 +17,18 @@
class GroupSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail')
user_count = serializers.IntegerField(read_only=True)
permissions = SerializedPKRelatedField(
source='object_permissions',
queryset=ObjectPermission.objects.all(),
serializer=ObjectPermissionSerializer,
nested=True,
required=False,
many=True
)

class Meta:
model = Group
fields = ('id', 'url', 'display', 'name', 'user_count')
fields = ('id', 'url', 'display', 'name', 'permissions', 'user_count')
brief_fields = ('id', 'url', 'display', 'name')


Expand All @@ -32,12 +41,20 @@ class UserSerializer(ValidatedModelSerializer):
required=False,
many=True
)
permissions = SerializedPKRelatedField(
source='object_permissions',
queryset=ObjectPermission.objects.all(),
serializer=ObjectPermissionSerializer,
nested=True,
required=False,
many=True
)

class Meta:
model = get_user_model()
fields = (
'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
'date_joined', 'last_login', 'groups',
'date_joined', 'last_login', 'groups', 'permissions',
)
brief_fields = ('id', 'url', 'display', 'username')
extra_kwargs = {
Expand Down
26 changes: 14 additions & 12 deletions netbox/users/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,19 +203,13 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if self.instance.pk:
# Populate assigned permissions
self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True)

# Password fields are optional for existing Users
self.fields['password'].required = False
self.fields['confirm_password'].required = False

def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)

# Update assigned permissions
instance.object_permissions.set(self.cleaned_data['object_permissions'])

# On edit, check if we have to save the password
if self.cleaned_data.get('password'):
instance.set_password(self.cleaned_data.get('password'))
Expand Down Expand Up @@ -260,14 +254,12 @@ def __init__(self, *args, **kwargs):
# Populate assigned users and permissions
if self.instance.pk:
self.fields['users'].initial = self.instance.users.values_list('id', flat=True)
self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True)

def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)

# Update assigned users and permissions
# Update assigned users
instance.users.set(self.cleaned_data['users'])
instance.object_permissions.set(self.cleaned_data['object_permissions'])

return instance

Expand Down Expand Up @@ -335,9 +327,10 @@ def __init__(self, *args, **kwargs):
# Make the actions field optional since the form uses it only for non-CRUD actions
self.fields['actions'].required = False

# Order group and user fields
self.fields['groups'].queryset = self.fields['groups'].queryset.order_by('name')
self.fields['users'].queryset = self.fields['users'].queryset.order_by('username')
# Populate assigned users and groups
if self.instance.pk:
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:
Expand Down Expand Up @@ -381,3 +374,12 @@ def clean(self):
raise forms.ValidationError({
'constraints': _('Invalid filter for {model}: {error}').format(model=model, error=e)
})

def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)

# Update assigned users and groups
instance.users.set(self.cleaned_data['users'])
instance.groups.set(self.cleaned_data['groups'])

return instance
122 changes: 122 additions & 0 deletions netbox/users/migrations/0008_flip_objectpermission_assignments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0007_objectpermission_update_object_types'),
]

operations = [
# Flip M2M assignments for ObjectPermission to Groups
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(
model_name='objectpermission',
name='groups',
),
migrations.AddField(
model_name='group',
name='object_permissions',
field=models.ManyToManyField(blank=True, related_name='groups', to='users.objectpermission'),
),
],
database_operations=[
# Rename table
migrations.RunSQL(
"ALTER TABLE users_objectpermission_groups"
" RENAME TO users_group_object_permissions"
),
migrations.RunSQL(
"ALTER TABLE users_objectpermission_groups_id_seq"
" RENAME TO users_group_object_permissions_id_seq"
),

# Rename constraints
migrations.RunSQL(
"ALTER TABLE users_group_object_permissions RENAME CONSTRAINT "
"users_objectpermissi_group_id_fb7ba6e0_fk_users_gro TO "
"users_group_object_p_group_id_90dd183a_fk_users_gro"
),
migrations.RunSQL(
"ALTER TABLE users_group_object_permissions RENAME CONSTRAINT "
"users_objectpermissi_objectpermission_id_2f7cc117_fk_users_obj TO "
"users_group_object_p_objectpermission_id_dd489dc4_fk_users_obj"
),

# Rename indexes
migrations.RunSQL(
"ALTER INDEX users_objectpermission_groups_pkey "
" RENAME TO users_group_object_permissions_pkey"
),
migrations.RunSQL(
"ALTER INDEX users_objectpermission_g_objectpermission_id_grou_3b62a39c_uniq "
" RENAME TO users_group_object_permi_group_id_objectpermissio_db1f8cbe_uniq"
),
migrations.RunSQL(
"ALTER INDEX users_objectpermission_groups_group_id_fb7ba6e0"
" RENAME TO users_group_object_permissions_group_id_90dd183a"
),
migrations.RunSQL(
"ALTER INDEX users_objectpermission_groups_objectpermission_id_2f7cc117"
" RENAME TO users_group_object_permissions_objectpermission_id_dd489dc4"
),
]
),

# Flip M2M assignments for ObjectPermission to Users
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(
model_name='objectpermission',
name='users',
),
migrations.AddField(
model_name='user',
name='object_permissions',
field=models.ManyToManyField(blank=True, related_name='users', to='users.objectpermission'),
),
],
database_operations=[
# Rename table
migrations.RunSQL(
"ALTER TABLE users_objectpermission_users"
" RENAME TO users_user_object_permissions"
),
migrations.RunSQL(
"ALTER TABLE users_objectpermission_users_id_seq"
" RENAME TO users_user_object_permissions_id_seq"
),

# Rename constraints
migrations.RunSQL(
"ALTER TABLE users_user_object_permissions RENAME CONSTRAINT "
"users_objectpermissi_objectpermission_id_78a9c2e6_fk_users_obj TO "
"users_user_object_pe_objectpermission_id_29b431b4_fk_users_obj"
),
migrations.RunSQL(
"ALTER TABLE users_user_object_permissions RENAME CONSTRAINT "
"users_objectpermission_users_user_id_16c0905d_fk_auth_user_id TO "
"users_user_object_permissions_user_id_9d647aac_fk_users_user_id"
),

# Rename indexes
migrations.RunSQL(
"ALTER INDEX users_objectpermission_users_pkey "
" RENAME TO users_user_object_permissions_pkey"
),
migrations.RunSQL(
"ALTER INDEX users_objectpermission_u_objectpermission_id_user_3a7db108_uniq "
" RENAME TO users_user_object_permis_user_id_objectpermission_0a98550e_uniq"
),
migrations.RunSQL(
"ALTER INDEX users_objectpermission_users_user_id_16c0905d"
" RENAME TO users_user_object_permissions_user_id_9d647aac"
),
migrations.RunSQL(
"ALTER INDEX users_objectpermission_users_objectpermission_id_78a9c2e6"
" RENAME TO users_user_object_permissions_objectpermission_id_29b431b4"
),
]
),
]
20 changes: 10 additions & 10 deletions netbox/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class Group(models.Model):
max_length=200,
blank=True
)
object_permissions = models.ManyToManyField(
to='users.ObjectPermission',
blank=True,
related_name='groups'
)

# Replicate legacy Django permissions support from stock Group model
# to ensure authentication backend compatibility
Expand Down Expand Up @@ -92,6 +97,11 @@ class User(AbstractUser):
related_name='users',
related_query_name='user'
)
object_permissions = models.ManyToManyField(
to='users.ObjectPermission',
blank=True,
related_name='users'
)

objects = UserManager()

Expand Down Expand Up @@ -387,16 +397,6 @@ class ObjectPermission(models.Model):
limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
related_name='object_permissions'
)
groups = models.ManyToManyField(
to='users.Group',
blank=True,
related_name='object_permissions'
)
users = models.ManyToManyField(
to=get_user_model(),
blank=True,
related_name='object_permissions'
)
actions = ArrayField(
base_field=models.CharField(max_length=30),
help_text=_("The list of actions granted by this permission")
Expand Down

0 comments on commit c8d9d93

Please sign in to comment.