Skip to content

#12795: Introduce a custom Group model #15304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions netbox/netbox/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
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,
)
Expand Down
6 changes: 3 additions & 3 deletions netbox/netbox/navigation/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,19 +392,19 @@
),
# 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']
Expand Down
3 changes: 1 addition & 2 deletions netbox/netbox/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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
Expand All @@ -12,7 +11,7 @@

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

Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/users/group.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ <h5 class="card-header">{% trans "Group" %}</h5>
<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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/users/objectpermission.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ <h5 class="card-header">{% trans "Assigned Users" %}</h5>
<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 %}
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/users/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ <h5 class="card-header">{% trans "User" %}</h5>
<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 %}
Expand Down
3 changes: 1 addition & 2 deletions netbox/users/api/nested_serializers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
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
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',
Expand Down
3 changes: 1 addition & 2 deletions netbox/users/api/serializers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,7 +9,7 @@

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 *


Expand Down
10 changes: 4 additions & 6 deletions netbox/users/api/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +13,7 @@

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
Expand All @@ -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

Expand Down
3 changes: 1 addition & 2 deletions netbox/users/filtersets.py
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion netbox/users/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class GroupImportForm(CSVModelForm):

class Meta:
model = NetBoxGroup
model = Group
fields = (
'name',
)
Expand Down
5 changes: 2 additions & 3 deletions netbox/users/forms/filtersets.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,7 +18,7 @@


class GroupFilterForm(NetBoxModelFilterSetForm):
model = NetBoxGroup
model = Group
fieldsets = (
(None, ('q', 'filter_id',)),
)
Expand Down
7 changes: 3 additions & 4 deletions netbox/users/forms/model_forms.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm):
)

class Meta:
model = NetBoxGroup
model = Group
fields = [
'name', 'users', 'object_permissions',
]
Expand All @@ -263,14 +262,14 @@ def __init__(self, *args, **kwargs):

# 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
Expand Down
6 changes: 3 additions & 3 deletions netbox/users/graphql/schema.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
2 changes: 1 addition & 1 deletion netbox/users/graphql/types.py
Original file line number Diff line number Diff line change
@@ -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__ = (
Expand Down
20 changes: 16 additions & 4 deletions netbox/users/migrations/0005_alter_user_table.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Generated by Django 5.0.1 on 2024-01-31 23:18

from django.db import migrations


Expand Down Expand Up @@ -27,12 +25,26 @@ class Migration(migrations.Migration):
]

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(
name='user',
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(
code=update_content_types,
reverse_code=migrations.RunPython.noop
Expand Down
80 changes: 80 additions & 0 deletions netbox/users/migrations/0006_custom_group_model.py
Original file line number Diff line number Diff line change
@@ -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',
),
]
Loading