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

#12795: Introduce a custom Group model (#15304)

* Rename sequences & indexes after renaming users table

* Migrate from auth.Group to a custom group model

* Delete original groups from auth_group table

* Update object & multi-object custom fields referencing the Group model

* Fix ContentType resolution

* Clean up obsolete logic for view/serializer resolution
Jeremy Stretch 1 год назад
Родитель
Сommit
c6a3fc2407

+ 2 - 2
netbox/netbox/authentication.py

@@ -4,13 +4,13 @@ from collections import defaultdict
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
 from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
-from django.contrib.auth.models import Group, AnonymousUser
+from django.contrib.auth.models import AnonymousUser
 from django.core.exceptions import ImproperlyConfigured
 from django.core.exceptions import ImproperlyConfigured
 from django.db.models import Q
 from django.db.models import Q
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from users.constants import CONSTRAINT_TOKEN_USER
 from users.constants import CONSTRAINT_TOKEN_USER
-from users.models import ObjectPermission
+from users.models import Group, ObjectPermission
 from utilities.permissions import (
 from utilities.permissions import (
     permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
     permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
 )
 )

+ 3 - 3
netbox/netbox/navigation/menu.py

@@ -392,19 +392,19 @@ ADMIN_MENU = Menu(
                 ),
                 ),
                 # Proxy model for auth.Group
                 # Proxy model for auth.Group
                 MenuItem(
                 MenuItem(
-                    link=f'users:netboxgroup_list',
+                    link=f'users:group_list',
                     link_text=_('Groups'),
                     link_text=_('Groups'),
                     permissions=[f'auth.view_group'],
                     permissions=[f'auth.view_group'],
                     staff_only=True,
                     staff_only=True,
                     buttons=(
                     buttons=(
                         MenuItemButton(
                         MenuItemButton(
-                            link=f'users:netboxgroup_add',
+                            link=f'users:group_add',
                             title='Add',
                             title='Add',
                             icon_class='mdi mdi-plus-thick',
                             icon_class='mdi mdi-plus-thick',
                             permissions=[f'auth.add_group']
                             permissions=[f'auth.add_group']
                         ),
                         ),
                         MenuItemButton(
                         MenuItemButton(
-                            link=f'users:netboxgroup_import',
+                            link=f'users:group_import',
                             title='Import',
                             title='Import',
                             icon_class='mdi mdi-upload',
                             icon_class='mdi mdi-upload',
                             permissions=[f'auth.add_group']
                             permissions=[f'auth.add_group']

+ 1 - 2
netbox/netbox/tests/test_authentication.py

@@ -2,7 +2,6 @@ import datetime
 
 
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.test import Client
 from django.test import Client
 from django.test.utils import override_settings
 from django.test.utils import override_settings
@@ -12,7 +11,7 @@ from rest_framework.test import APIClient
 
 
 from dcim.models import Site
 from dcim.models import Site
 from ipam.models import Prefix
 from ipam.models import Prefix
-from users.models import ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token
 from utilities.testing import TestCase
 from utilities.testing import TestCase
 from utilities.testing.api import APITestCase
 from utilities.testing.api import APITestCase
 
 

+ 1 - 1
netbox/templates/users/group.html

@@ -24,7 +24,7 @@
       <div class="card">
       <div class="card">
         <h5 class="card-header">{% trans "Users" %}</h5>
         <h5 class="card-header">{% trans "Users" %}</h5>
         <div class="list-group list-group-flush">
         <div class="list-group list-group-flush">
-          {% for user in object.user_set.all %}
+          {% for user in object.users.all %}
             <a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
             <a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
           {% empty %}
           {% empty %}
             <div class="list-group-item text-muted">{% trans "None" %}</div>
             <div class="list-group-item text-muted">{% trans "None" %}</div>

+ 1 - 1
netbox/templates/users/objectpermission.html

@@ -82,7 +82,7 @@
         <h5 class="card-header">{% trans "Assigned Groups" %}</h5>
         <h5 class="card-header">{% trans "Assigned Groups" %}</h5>
         <div class="list-group list-group-flush">
         <div class="list-group list-group-flush">
           {% for group in object.groups.all %}
           {% for group in object.groups.all %}
-            <a href="{% url 'users:netboxgroup' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
+            <a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
           {% empty %}
           {% empty %}
             <div class="list-group-item text-muted">{% trans "None" %}</div>
             <div class="list-group-item text-muted">{% trans "None" %}</div>
           {% endfor %}
           {% endfor %}

+ 1 - 1
netbox/templates/users/user.html

@@ -53,7 +53,7 @@
         <h5 class="card-header">{% trans "Assigned Groups" %}</h5>
         <h5 class="card-header">{% trans "Assigned Groups" %}</h5>
         <div class="list-group list-group-flush">
         <div class="list-group list-group-flush">
           {% for group in object.groups.all %}
           {% for group in object.groups.all %}
-            <a href="{% url 'users:netboxgroup' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
+            <a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
           {% empty %}
           {% empty %}
             <div class="list-group-item text-muted">{% trans "None" %}</div>
             <div class="list-group-item text-muted">{% trans "None" %}</div>
           {% endfor %}
           {% endfor %}

+ 1 - 2
netbox/users/api/nested_serializers.py

@@ -1,5 +1,4 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
@@ -7,7 +6,7 @@ from rest_framework import serializers
 
 
 from netbox.api.fields import ContentTypeField
 from netbox.api.fields import ContentTypeField
 from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.serializers import WritableNestedSerializer
-from users.models import ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token
 
 
 __all__ = [
 __all__ = [
     'NestedGroupSerializer',
     'NestedGroupSerializer',

+ 1 - 2
netbox/users/api/serializers.py

@@ -1,7 +1,6 @@
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth import authenticate
 from django.contrib.auth import authenticate
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
@@ -10,7 +9,7 @@ from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
 
 
 from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
 from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
 from netbox.api.serializers import ValidatedModelSerializer
 from netbox.api.serializers import ValidatedModelSerializer
-from users.models import ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 
 

+ 4 - 6
netbox/users/api/views.py

@@ -1,11 +1,9 @@
 import logging
 import logging
-from django.contrib.auth import authenticate
+
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.db.models import Count
 from django.db.models import Count
-from drf_spectacular.utils import extend_schema
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
-from rest_framework.exceptions import AuthenticationFailed
+from drf_spectacular.utils import extend_schema
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
@@ -15,7 +13,7 @@ from rest_framework.viewsets import ViewSet
 
 
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet
 from users import filtersets
 from users import filtersets
-from users.models import ObjectPermission, Token, UserConfig
+from users.models import Group, ObjectPermission, Token, UserConfig
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 from utilities.utils import deepmerge
 from utilities.utils import deepmerge
 from . import serializers
 from . import serializers
@@ -40,7 +38,7 @@ class UserViewSet(NetBoxModelViewSet):
 
 
 
 
 class GroupViewSet(NetBoxModelViewSet):
 class GroupViewSet(NetBoxModelViewSet):
-    queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
+    queryset = Group.objects.annotate(user_count=Count('user'))
     serializer_class = serializers.GroupSerializer
     serializer_class = serializers.GroupSerializer
     filterset_class = filtersets.GroupFilterSet
     filterset_class = filtersets.GroupFilterSet
 
 

+ 1 - 2
netbox/users/filtersets.py

@@ -1,11 +1,10 @@
 import django_filters
 import django_filters
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.db.models import Q
 from django.db.models import Q
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from netbox.filtersets import BaseFilterSet
 from netbox.filtersets import BaseFilterSet
-from users.models import ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token
 
 
 __all__ = (
 __all__ = (
     'GroupFilterSet',
     'GroupFilterSet',

+ 1 - 1
netbox/users/forms/bulk_import.py

@@ -14,7 +14,7 @@ __all__ = (
 class GroupImportForm(CSVModelForm):
 class GroupImportForm(CSVModelForm):
 
 
     class Meta:
     class Meta:
-        model = NetBoxGroup
+        model = Group
         fields = (
         fields = (
             'name',
             'name',
         )
         )

+ 2 - 3
netbox/users/forms/filtersets.py

@@ -1,11 +1,10 @@
 from django import forms
 from django import forms
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms.mixins import SavedFiltersMixin
 from netbox.forms.mixins import SavedFiltersMixin
-from users.models import NetBoxGroup, User, ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token, User
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms.fields import DynamicModelMultipleChoiceField
 from utilities.forms.fields import DynamicModelMultipleChoiceField
 from utilities.forms.widgets import DateTimePicker
 from utilities.forms.widgets import DateTimePicker
@@ -19,7 +18,7 @@ __all__ = (
 
 
 
 
 class GroupFilterForm(NetBoxModelFilterSetForm):
 class GroupFilterForm(NetBoxModelFilterSetForm):
-    model = NetBoxGroup
+    model = Group
     fieldsets = (
     fieldsets = (
         (None, ('q', 'filter_id',)),
         (None, ('q', 'filter_id',)),
     )
     )

+ 3 - 4
netbox/users/forms/model_forms.py

@@ -1,7 +1,6 @@
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms import SimpleArrayField
 from django.contrib.postgres.forms import SimpleArrayField
 from django.core.exceptions import FieldError
 from django.core.exceptions import FieldError
@@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm):
     )
     )
 
 
     class Meta:
     class Meta:
-        model = NetBoxGroup
+        model = Group
         fields = [
         fields = [
             'name', 'users', 'object_permissions',
             'name', 'users', 'object_permissions',
         ]
         ]
@@ -263,14 +262,14 @@ class GroupForm(forms.ModelForm):
 
 
         # Populate assigned users and permissions
         # Populate assigned users and permissions
         if self.instance.pk:
         if self.instance.pk:
-            self.fields['users'].initial = self.instance.user_set.values_list('id', flat=True)
+            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)
             self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True)
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         instance = super().save(*args, **kwargs)
         instance = super().save(*args, **kwargs)
 
 
         # Update assigned users and permissions
         # Update assigned users and permissions
-        instance.user_set.set(self.cleaned_data['users'])
+        instance.users.set(self.cleaned_data['users'])
         instance.object_permissions.set(self.cleaned_data['object_permissions'])
         instance.object_permissions.set(self.cleaned_data['object_permissions'])
 
 
         return instance
         return instance

+ 3 - 3
netbox/users/graphql/schema.py

@@ -1,10 +1,10 @@
 import graphene
 import graphene
-
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
+
 from netbox.graphql.fields import ObjectField, ObjectListField
 from netbox.graphql.fields import ObjectField, ObjectListField
-from .types import *
+from users.models import Group
 from utilities.graphql_optimizer import gql_query_optimizer
 from utilities.graphql_optimizer import gql_query_optimizer
+from .types import *
 
 
 
 
 class UsersQuery(graphene.ObjectType):
 class UsersQuery(graphene.ObjectType):

+ 1 - 1
netbox/users/graphql/types.py

@@ -1,8 +1,8 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from graphene_django import DjangoObjectType
 from graphene_django import DjangoObjectType
 
 
 from users import filtersets
 from users import filtersets
+from users.models import Group
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 
 
 __all__ = (
 __all__ = (

+ 16 - 4
netbox/users/migrations/0005_alter_user_table.py

@@ -1,5 +1,3 @@
-# Generated by Django 5.0.1 on 2024-01-31 23:18
-
 from django.db import migrations
 from django.db import migrations
 
 
 
 
@@ -27,12 +25,26 @@ class Migration(migrations.Migration):
     ]
     ]
 
 
     operations = [
     operations = [
-        # 0001_squashed had model with db_table=auth_user - now we switch it
-        # to None to use the default Django resolution (users.user)
+        # The User table was originally created as 'auth_user'. Now we nullify the model's
+        # db_table option, so that it defaults to the app & model name (users_user). This
+        # causes the database table to be renamed.
         migrations.AlterModelTable(
         migrations.AlterModelTable(
             name='user',
             name='user',
             table=None,
             table=None,
         ),
         ),
+
+        # Rename auth_user_* sequences
+        migrations.RunSQL("ALTER TABLE auth_user_groups_id_seq RENAME TO users_user_groups_id_seq"),
+        migrations.RunSQL("ALTER TABLE auth_user_id_seq RENAME TO users_user_id_seq"),
+        migrations.RunSQL("ALTER TABLE auth_user_user_permissions_id_seq RENAME TO users_user_user_permissions_id_seq"),
+
+        # Rename auth_user_* indexes
+        migrations.RunSQL("ALTER INDEX auth_user_pkey RENAME TO users_user_pkey"),
+        # Hash is deterministic; generated via schema_editor._create_index_name()
+        migrations.RunSQL("ALTER INDEX auth_user_username_6821ab7c_like RENAME TO users_user_username_06e46fe6_like"),
+        migrations.RunSQL("ALTER INDEX auth_user_username_key RENAME TO users_user_username_key"),
+
+        # Update ContentTypes
         migrations.RunPython(
         migrations.RunPython(
             code=update_content_types,
             code=update_content_types,
             reverse_code=migrations.RunPython.noop
             reverse_code=migrations.RunPython.noop

+ 80 - 0
netbox/users/migrations/0006_custom_group_model.py

@@ -0,0 +1,80 @@
+import users.models
+from django.db import migrations, models
+
+
+def update_custom_fields(apps, schema_editor):
+    """
+    Update any CustomFields referencing the old Group model to use the new model.
+    """
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+    CustomField = apps.get_model('extras', 'CustomField')
+    Group = apps.get_model('users', 'Group')
+
+    if old_ct := ContentType.objects.filter(app_label='users', model='netboxgroup').first():
+        new_ct = ContentType.objects.get_for_model(Group)
+        CustomField.objects.filter(object_type=old_ct).update(object_type=new_ct)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0005_alter_user_table'),
+    ]
+
+    operations = [
+        # Create the new Group model & table
+        migrations.CreateModel(
+            name='Group',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('name', models.CharField(max_length=150, unique=True)),
+                ('description', models.CharField(blank=True, max_length=200)),
+                ('permissions', models.ManyToManyField(blank=True, related_name='groups', related_query_name='group', to='auth.permission')),
+            ],
+            options={
+                'verbose_name': 'group',
+                'verbose_name_plural': 'groups',
+            },
+            managers=[
+                ('objects', users.models.NetBoxGroupManager()),
+            ],
+        ),
+
+        # Copy existing groups from the old table into the new one
+        migrations.RunSQL(
+            "INSERT INTO users_group (SELECT id, name, '' AS description FROM auth_group)"
+        ),
+
+        # Update the sequence for group ID values
+        migrations.RunSQL(
+            "SELECT setval('users_group_id_seq', (SELECT MAX(id) FROM users_group))"
+        ),
+
+        # Update the "groups" M2M fields on User & ObjectPermission
+        migrations.AlterField(
+            model_name='user',
+            name='groups',
+            field=models.ManyToManyField(blank=True, related_name='users', related_query_name='user', to='users.group'),
+        ),
+        migrations.AlterField(
+            model_name='objectpermission',
+            name='groups',
+            field=models.ManyToManyField(blank=True, related_name='object_permissions', to='users.group'),
+        ),
+
+        # Delete groups from the old table
+        migrations.RunSQL(
+            "DELETE from auth_group"
+        ),
+
+        # Update custom fields
+        migrations.RunPython(
+            code=update_custom_fields,
+            reverse_code=migrations.RunPython.noop
+        ),
+
+        # Delete the proxy model
+        migrations.DeleteModel(
+            name='NetBoxGroup',
+        ),
+    ]

+ 56 - 28
netbox/users/models.py

@@ -4,7 +4,12 @@ import os
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import (
 from django.contrib.auth.models import (
-    AbstractUser, Group, GroupManager, User as DjangoUser, UserManager as DjangoUserManager
+    AbstractUser,
+    Group as DjangoGroup,
+    GroupManager,
+    Permission,
+    User as DjangoUser,
+    UserManager as DjangoUserManager
 )
 )
 from django.contrib.postgres.fields import ArrayField
 from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
@@ -25,7 +30,7 @@ from utilities.utils import flatten_dict
 from .constants import *
 from .constants import *
 
 
 __all__ = (
 __all__ = (
-    'NetBoxGroup',
+    'Group',
     'ObjectPermission',
     'ObjectPermission',
     'Token',
     'Token',
     'User',
     'User',
@@ -33,22 +38,61 @@ __all__ = (
 )
 )
 
 
 
 
-#
-# Proxies for Django's User and Group models
-#
-
-class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
+class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
     pass
     pass
 
 
 
 
-class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
+class Group(models.Model):
+    name = models.CharField(
+        verbose_name=_('name'),
+        max_length=150,
+        unique=True
+    )
+    description = models.CharField(
+        verbose_name=_('description'),
+        max_length=200,
+        blank=True
+    )
+
+    # Replicate legacy Django permissions support from stock Group model
+    # to ensure authentication backend compatibility
+    permissions = models.ManyToManyField(
+        Permission,
+        verbose_name=_("permissions"),
+        blank=True,
+        related_name='groups',
+        related_query_name='group'
+    )
+
+    objects = NetBoxGroupManager()
+
+    class Meta:
+        verbose_name = _('group')
+        verbose_name_plural = _('groups')
+
+    def __str__(self):
+        return self.name
+
+    def get_absolute_url(self):
+        return reverse('users:group', args=[self.pk])
+
+    def natural_key(self):
+        return (self.name,)
+
+
+class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
     pass
     pass
 
 
 
 
 class User(AbstractUser):
 class User(AbstractUser):
-    """
-    Proxy contrib.auth.models.User for the UI
-    """
+    groups = models.ManyToManyField(
+        to='users.Group',
+        verbose_name=_('groups'),
+        blank=True,
+        related_name='users',
+        related_query_name='user'
+    )
+
     objects = UserManager()
     objects = UserManager()
 
 
     class Meta:
     class Meta:
@@ -68,22 +112,6 @@ class User(AbstractUser):
             raise ValidationError(_("A user with this username already exists."))
             raise ValidationError(_("A user with this username already exists."))
 
 
 
 
-class NetBoxGroup(Group):
-    """
-    Proxy contrib.auth.models.User for the UI
-    """
-    objects = NetBoxGroupManager()
-
-    class Meta:
-        proxy = True
-        ordering = ('name',)
-        verbose_name = _('group')
-        verbose_name_plural = _('groups')
-
-    def get_absolute_url(self):
-        return reverse('users:netboxgroup', args=[self.pk])
-
-
 #
 #
 # User preferences
 # User preferences
 #
 #
@@ -360,7 +388,7 @@ class ObjectPermission(models.Model):
         related_name='object_permissions'
         related_name='object_permissions'
     )
     )
     groups = models.ManyToManyField(
     groups = models.ManyToManyField(
-        to=Group,
+        to='users.Group',
         blank=True,
         blank=True,
         related_name='object_permissions'
         related_name='object_permissions'
     )
     )

+ 4 - 4
netbox/users/tables.py

@@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
 
 
 from account.tables import UserTokenTable
 from account.tables import UserTokenTable
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from users.models import NetBoxGroup, User, ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token, User
 
 
 __all__ = (
 __all__ = (
     'GroupTable',
     'GroupTable',
@@ -33,7 +33,7 @@ class UserTable(NetBoxTable):
     )
     )
     groups = columns.ManyToManyColumn(
     groups = columns.ManyToManyColumn(
         verbose_name=_('Groups'),
         verbose_name=_('Groups'),
-        linkify_item=('users:netboxgroup', {'pk': tables.A('pk')})
+        linkify_item=('users:group', {'pk': tables.A('pk')})
     )
     )
     is_active = columns.BooleanColumn(
     is_active = columns.BooleanColumn(
         verbose_name=_('Is Active'),
         verbose_name=_('Is Active'),
@@ -67,7 +67,7 @@ class GroupTable(NetBoxTable):
     )
     )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
-        model = NetBoxGroup
+        model = Group
         fields = (
         fields = (
             'pk', 'id', 'name', 'users_count',
             'pk', 'id', 'name', 'users_count',
         )
         )
@@ -107,7 +107,7 @@ class ObjectPermissionTable(NetBoxTable):
     )
     )
     groups = columns.ManyToManyColumn(
     groups = columns.ManyToManyColumn(
         verbose_name=_('Groups'),
         verbose_name=_('Groups'),
-        linkify_item=('users:netboxgroup', {'pk': tables.A('pk')})
+        linkify_item=('users:group', {'pk': tables.A('pk')})
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
         actions=('edit', 'delete'),
         actions=('edit', 'delete'),

+ 1 - 2
netbox/users/tests/test_api.py

@@ -1,9 +1,8 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.urls import reverse
 
 
-from users.models import ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token
 from utilities.testing import APIViewTestCases, APITestCase, create_test_user
 from utilities.testing import APIViewTestCases, APITestCase, create_test_user
 from utilities.utils import deepmerge
 from utilities.utils import deepmerge
 
 

+ 1 - 2
netbox/users/tests/test_filtersets.py

@@ -1,13 +1,12 @@
 import datetime
 import datetime
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 from django.utils.timezone import make_aware
 from django.utils.timezone import make_aware
 
 
 from users import filtersets
 from users import filtersets
-from users.models import ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token
 from utilities.testing import BaseFilterSetTests
 from utilities.testing import BaseFilterSetTests
 
 
 User = get_user_model()
 User = get_user_model()

+ 1 - 2
netbox/users/tests/test_views.py

@@ -1,4 +1,3 @@
-from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 
 
 from users.models import *
 from users.models import *
@@ -70,7 +69,7 @@ class GroupTestCase(
     ViewTestCases.BulkImportObjectsViewTestCase,
     ViewTestCases.BulkImportObjectsViewTestCase,
     ViewTestCases.BulkDeleteObjectsViewTestCase,
     ViewTestCases.BulkDeleteObjectsViewTestCase,
 ):
 ):
-    model = NetBoxGroup
+    model = Group
     maxDiff = None
     maxDiff = None
 
 
     @classmethod
     @classmethod

+ 5 - 5
netbox/users/urls.py

@@ -23,11 +23,11 @@ urlpatterns = [
     path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
     path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
 
 
     # Groups
     # Groups
-    path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'),
-    path('groups/add/', views.GroupEditView.as_view(), name='netboxgroup_add'),
-    path('groups/import/', views.GroupBulkImportView.as_view(), name='netboxgroup_import'),
-    path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='netboxgroup_bulk_delete'),
-    path('groups/<int:pk>/', include(get_model_urls('users', 'netboxgroup'))),
+    path('groups/', views.GroupListView.as_view(), name='group_list'),
+    path('groups/add/', views.GroupEditView.as_view(), name='group_add'),
+    path('groups/import/', views.GroupBulkImportView.as_view(), name='group_import'),
+    path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='group_bulk_delete'),
+    path('groups/<int:pk>/', include(get_model_urls('users', 'group'))),
 
 
     # Permissions
     # Permissions
     path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'),
     path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'),

+ 10 - 10
netbox/users/views.py

@@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable
 from netbox.views import generic
 from netbox.views import generic
 from utilities.views import register_model_view
 from utilities.views import register_model_view
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
-from .models import NetBoxGroup, User, ObjectPermission, Token
+from .models import Group, User, ObjectPermission, Token
 
 
 
 
 #
 #
@@ -110,36 +110,36 @@ class UserBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class GroupListView(generic.ObjectListView):
 class GroupListView(generic.ObjectListView):
-    queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
+    queryset = Group.objects.annotate(users_count=Count('user'))
     filterset = filtersets.GroupFilterSet
     filterset = filtersets.GroupFilterSet
     filterset_form = forms.GroupFilterForm
     filterset_form = forms.GroupFilterForm
     table = tables.GroupTable
     table = tables.GroupTable
 
 
 
 
-@register_model_view(NetBoxGroup)
+@register_model_view(Group)
 class GroupView(generic.ObjectView):
 class GroupView(generic.ObjectView):
-    queryset = NetBoxGroup.objects.all()
+    queryset = Group.objects.all()
     template_name = 'users/group.html'
     template_name = 'users/group.html'
 
 
 
 
-@register_model_view(NetBoxGroup, 'edit')
+@register_model_view(Group, 'edit')
 class GroupEditView(generic.ObjectEditView):
 class GroupEditView(generic.ObjectEditView):
-    queryset = NetBoxGroup.objects.all()
+    queryset = Group.objects.all()
     form = forms.GroupForm
     form = forms.GroupForm
 
 
 
 
-@register_model_view(NetBoxGroup, 'delete')
+@register_model_view(Group, 'delete')
 class GroupDeleteView(generic.ObjectDeleteView):
 class GroupDeleteView(generic.ObjectDeleteView):
-    queryset = NetBoxGroup.objects.all()
+    queryset = Group.objects.all()
 
 
 
 
 class GroupBulkImportView(generic.BulkImportView):
 class GroupBulkImportView(generic.BulkImportView):
-    queryset = NetBoxGroup.objects.all()
+    queryset = Group.objects.all()
     model_form = forms.GroupImportForm
     model_form = forms.GroupImportForm
 
 
 
 
 class GroupBulkDeleteView(generic.BulkDeleteView):
 class GroupBulkDeleteView(generic.BulkDeleteView):
-    queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
+    queryset = Group.objects.annotate(users_count=Count('user'))
     filterset = filtersets.GroupFilterSet
     filterset = filtersets.GroupFilterSet
     table = tables.GroupTable
     table = tables.GroupTable
 
 

+ 3 - 13
netbox/utilities/api.py

@@ -31,23 +31,13 @@ def get_serializer_for_model(model, prefix=''):
     """
     """
     Dynamically resolve and return the appropriate serializer for a model.
     Dynamically resolve and return the appropriate serializer for a model.
     """
     """
-    app_name, model_name = model._meta.label.split('.')
-    # Serializers for Django's auth models are in the users app
-    if app_name == 'auth':
-        app_name = 'users'
-    # Account for changes using Proxy model
-    if app_name == 'users':
-        if model_name == 'NetBoxUser':
-            model_name = 'User'
-        elif model_name == 'NetBoxGroup':
-            model_name = 'Group'
-
-    serializer_name = f'{app_name}.api.serializers.{prefix}{model_name}Serializer'
+    app_label, model_name = model._meta.label.split('.')
+    serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
     try:
     try:
         return dynamic_import(serializer_name)
         return dynamic_import(serializer_name)
     except AttributeError:
     except AttributeError:
         raise SerializerNotFound(
         raise SerializerNotFound(
-            f"Could not determine serializer for {app_name}.{model_name} with prefix '{prefix}'"
+            f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'"
         )
         )
 
 
 
 

+ 4 - 14
netbox/utilities/utils.py

@@ -1,11 +1,12 @@
 import datetime
 import datetime
 import decimal
 import decimal
 import json
 import json
-import nh3
 import re
 import re
 from decimal import Decimal
 from decimal import Decimal
 from itertools import count, groupby
 from itertools import count, groupby
+from urllib.parse import urlencode
 
 
+import nh3
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core import serializers
 from django.core import serializers
 from django.db.models import Count, ManyToOneRel, OuterRef, Subquery
 from django.db.models import Count, ManyToOneRel, OuterRef, Subquery
@@ -23,7 +24,6 @@ from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
 from extras.utils import is_taggable
 from extras.utils import is_taggable
 from netbox.config import get_config
 from netbox.config import get_config
 from netbox.plugins import PluginConfig
 from netbox.plugins import PluginConfig
-from urllib.parse import urlencode
 from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
 from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
 from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS
 from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS
 
 
@@ -48,26 +48,16 @@ def get_viewname(model, action=None, rest_api=False):
     model_name = model._meta.model_name
     model_name = model._meta.model_name
 
 
     if rest_api:
     if rest_api:
+        viewname = f'{app_label}-api:{model_name}'
         if is_plugin:
         if is_plugin:
-            viewname = f'plugins-api:{app_label}-api:{model_name}'
-        else:
-            # Alter the app_label for group and user model_name to point to users app
-            if app_label == 'auth' and model_name in ['group', 'user']:
-                app_label = 'users'
-            if app_label == 'users' and model._meta.proxy and model_name in ['netboxuser', 'netboxgroup']:
-                model_name = model._meta.proxy_for_model._meta.model_name
-
-            viewname = f'{app_label}-api:{model_name}'
-        # Append the action, if any
+            viewname = f'plugins-api:{viewname}'
         if action:
         if action:
             viewname = f'{viewname}-{action}'
             viewname = f'{viewname}-{action}'
 
 
     else:
     else:
         viewname = f'{app_label}:{model_name}'
         viewname = f'{app_label}:{model_name}'
-        # Prepend the plugins namespace if this is a plugin model
         if is_plugin:
         if is_plugin:
             viewname = f'plugins:{viewname}'
             viewname = f'plugins:{viewname}'
-        # Append the action, if any
         if action:
         if action:
             viewname = f'{viewname}_{action}'
             viewname = f'{viewname}_{action}'