Migrate from auth.Group to a custom group model

This commit is contained in:
Jeremy Stretch 2024-02-28 15:09:17 -05:00
parent bc2b1e0709
commit 9f64e7d88f
25 changed files with 162 additions and 88 deletions

View File

@ -4,13 +4,13 @@ from collections import defaultdict
from django.conf import settings
from django.contrib.auth import get_user_model
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.db.models import Q
from django.utils.translation import gettext_lazy as _
from users.constants import CONSTRAINT_TOKEN_USER
from users.models import ObjectPermission
from users.models import Group, ObjectPermission
from utilities.permissions import (
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
)

View File

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

View File

@ -2,7 +2,6 @@ import datetime
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.test import Client
from django.test.utils import override_settings
@ -12,7 +11,7 @@ from rest_framework.test import APIClient
from dcim.models import Site
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.api import APITestCase

View File

@ -24,7 +24,7 @@
<div class="card">
<h5 class="card-header">{% trans "Users" %}</h5>
<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>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>

View File

@ -82,7 +82,7 @@
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
<div class="list-group list-group-flush">
{% 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 %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}

View File

@ -53,7 +53,7 @@
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
<div class="list-group list-group-flush">
{% 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 %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}

View File

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

View File

@ -1,7 +1,6 @@
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
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.serializers import ValidatedModelSerializer
from users.models import ObjectPermission, Token
from users.models import Group, ObjectPermission, Token
from .nested_serializers import *

View File

@ -1,11 +1,9 @@
import logging
from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.db.models import Count
from drf_spectacular.utils import extend_schema
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.response import Response
from rest_framework.routers import APIRootView
@ -15,7 +13,7 @@ from rest_framework.viewsets import ViewSet
from netbox.api.viewsets import NetBoxModelViewSet
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.utils import deepmerge
from . import serializers
@ -40,7 +38,7 @@ class UserViewSet(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
filterset_class = filtersets.GroupFilterSet

View File

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

View File

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

View File

@ -1,11 +1,10 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelFilterSetForm
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.fields import DynamicModelMultipleChoiceField
from utilities.forms.widgets import DateTimePicker
@ -19,7 +18,7 @@ __all__ = (
class GroupFilterForm(NetBoxModelFilterSetForm):
model = NetBoxGroup
model = Group
fieldsets = (
(None, ('q', 'filter_id',)),
)

View File

@ -1,7 +1,6 @@
from django import forms
from django.conf import settings
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.postgres.forms import SimpleArrayField
from django.core.exceptions import FieldError
@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm):
)
class Meta:
model = NetBoxGroup
model = Group
fields = [
'name', 'users', 'object_permissions',
]
@ -263,14 +262,14 @@ class GroupForm(forms.ModelForm):
# Populate assigned users and permissions
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)
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# 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'])
return instance

View File

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

View File

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

View File

@ -0,0 +1,55 @@
import users.models
from django.db import migrations, models
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'),
),
migrations.DeleteModel(
name='NetBoxGroup',
),
]

View File

@ -4,7 +4,12 @@ import os
from django.conf import settings
from django.contrib.auth import get_user_model
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.core.exceptions import ValidationError
@ -25,7 +30,7 @@ from utilities.utils import flatten_dict
from .constants import *
__all__ = (
'NetBoxGroup',
'Group',
'ObjectPermission',
'Token',
'User',
@ -33,22 +38,61 @@ __all__ = (
)
#
# Proxies for Django's User and Group models
#
class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
pass
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
class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
pass
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()
class Meta:
@ -68,22 +112,6 @@ class User(AbstractUser):
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
#
@ -360,7 +388,7 @@ class ObjectPermission(models.Model):
related_name='object_permissions'
)
groups = models.ManyToManyField(
to=Group,
to='users.Group',
blank=True,
related_name='object_permissions'
)

View File

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

View File

@ -1,9 +1,8 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
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.utils import deepmerge

View File

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

View File

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

View File

@ -23,11 +23,11 @@ urlpatterns = [
path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
# 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
path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'),

View File

@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable
from netbox.views import generic
from utilities.views import register_model_view
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):
queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
queryset = Group.objects.annotate(users_count=Count('user'))
filterset = filtersets.GroupFilterSet
filterset_form = forms.GroupFilterForm
table = tables.GroupTable
@register_model_view(NetBoxGroup)
@register_model_view(Group)
class GroupView(generic.ObjectView):
queryset = NetBoxGroup.objects.all()
queryset = Group.objects.all()
template_name = 'users/group.html'
@register_model_view(NetBoxGroup, 'edit')
@register_model_view(Group, 'edit')
class GroupEditView(generic.ObjectEditView):
queryset = NetBoxGroup.objects.all()
queryset = Group.objects.all()
form = forms.GroupForm
@register_model_view(NetBoxGroup, 'delete')
@register_model_view(Group, 'delete')
class GroupDeleteView(generic.ObjectDeleteView):
queryset = NetBoxGroup.objects.all()
queryset = Group.objects.all()
class GroupBulkImportView(generic.BulkImportView):
queryset = NetBoxGroup.objects.all()
queryset = Group.objects.all()
model_form = forms.GroupImportForm
class GroupBulkDeleteView(generic.BulkDeleteView):
queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
queryset = Group.objects.annotate(users_count=Count('user'))
filterset = filtersets.GroupFilterSet
table = tables.GroupTable

View File

@ -32,6 +32,7 @@ def get_serializer_for_model(model, prefix=''):
Dynamically resolve and return the appropriate serializer for a model.
"""
app_name, model_name = model._meta.label.split('.')
# TODO: Remove this logic
# Serializers for Django's auth models are in the users app
if app_name == 'auth':
app_name = 'users'

View File

@ -51,6 +51,7 @@ def get_viewname(model, action=None, rest_api=False):
if is_plugin:
viewname = f'plugins-api:{app_label}-api:{model_name}'
else:
# TODO: Remove this logic
# 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'