diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 8ea133528..0262a5f98 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -377,19 +377,19 @@ ADMIN_MENU = Menu( items=( # Proxy model for auth.User MenuItem( - link=f'users:netboxuser_list', + link=f'users:user_list', link_text=_('Users'), permissions=[f'auth.view_user'], staff_only=True, buttons=( MenuItemButton( - link=f'users:netboxuser_add', + link=f'users:user_add', title='Add', icon_class='mdi mdi-plus-thick', permissions=[f'auth.add_user'] ), MenuItemButton( - link=f'users:netboxuser_import', + link=f'users:user_import', title='Import', icon_class='mdi mdi-upload', permissions=[f'auth.add_user'] diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8e40686c1..1de69f32d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -455,6 +455,8 @@ AUTHENTICATION_BACKENDS = [ 'netbox.authentication.ObjectPermissionBackend', ] +AUTH_USER_MODEL = 'users.User' + # Time zones USE_TZ = True @@ -595,6 +597,8 @@ for param in dir(configuration): SOCIAL_AUTH_JSONFIELD_ENABLED = True SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username' +SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL + # # Django Prometheus # diff --git a/netbox/templates/users/group.html b/netbox/templates/users/group.html index fc84aa789..27b4707fb 100644 --- a/netbox/templates/users/group.html +++ b/netbox/templates/users/group.html @@ -25,7 +25,7 @@
{% trans "Users" %}
{% for user in object.user_set.all %} - {{ user }} + {{ user }} {% empty %}
{% trans "None" %}
{% endfor %} diff --git a/netbox/templates/users/objectpermission.html b/netbox/templates/users/objectpermission.html index fbf3ccc6b..9a222ba80 100644 --- a/netbox/templates/users/objectpermission.html +++ b/netbox/templates/users/objectpermission.html @@ -72,7 +72,7 @@
{% trans "Assigned Users" %}
{% for user in object.users.all %} - {{ user }} + {{ user }} {% empty %}
{% trans "None" %}
{% endfor %} diff --git a/netbox/templates/users/token.html b/netbox/templates/users/token.html index 91ac9c5e5..2d1858323 100644 --- a/netbox/templates/users/token.html +++ b/netbox/templates/users/token.html @@ -20,7 +20,7 @@ {% trans "User" %} - {{ object.user }} + {{ object.user }} diff --git a/netbox/users/admin.py b/netbox/users/admin.py deleted file mode 100644 index a82ec4997..000000000 --- a/netbox/users/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.models import Group, User - -# Unregister Django's built-in Group and User admin views -admin.site.unregister(Group) -admin.site.unregister(User) diff --git a/netbox/users/apps.py b/netbox/users/apps.py index b8d67f1c3..95d9e03a0 100644 --- a/netbox/users/apps.py +++ b/netbox/users/apps.py @@ -6,3 +6,14 @@ class UsersConfig(AppConfig): def ready(self): import users.signals + from .models import NetBoxGroup, ObjectPermission, Token, User, UserConfig + from netbox.models.features import _register_features + + # have to register these manually as the signal handler for class_prepared does + # not get registered until after these models are loaded. Any models defined in + # users.models should be registered here. + _register_features(NetBoxGroup) + _register_features(ObjectPermission) + _register_features(Token) + _register_features(User) + _register_features(UserConfig) diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index f88f71bf0..c56beff14 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -17,7 +17,7 @@ __all__ = ( class UserBulkEditForm(forms.Form): pk = forms.ModelMultipleChoiceField( - queryset=NetBoxUser.objects.all(), + queryset=User.objects.all(), widget=forms.MultipleHiddenInput ) first_name = forms.CharField( @@ -46,7 +46,7 @@ class UserBulkEditForm(forms.Form): label=_('Superuser status') ) - model = NetBoxUser + model = User fieldsets = ( (None, ('first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser')), ) diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index d1f03ff3c..055998c69 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -23,7 +23,7 @@ class GroupImportForm(CSVModelForm): class UserImportForm(CSVModelForm): class Meta: - model = NetBoxUser + model = User fields = ( 'username', 'first_name', 'last_name', 'email', 'password', 'is_staff', 'is_active', 'is_superuser' diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 4ae2bd729..c127e2144 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin -from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token +from users.models import NetBoxGroup, User, ObjectPermission, Token from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.widgets import DateTimePicker @@ -26,7 +26,7 @@ class GroupFilterForm(NetBoxModelFilterSetForm): class UserFilterForm(NetBoxModelFilterSetForm): - model = NetBoxUser + model = User fieldsets = ( (None, ('q', 'filter_id',)), (_('Group'), ('group_id',)), diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index d5742587f..11874e929 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -198,7 +198,7 @@ class UserForm(forms.ModelForm): ) class Meta: - model = NetBoxUser + model = User fields = [ 'username', 'first_name', 'last_name', 'email', 'groups', 'object_permissions', 'is_active', 'is_staff', 'is_superuser', diff --git a/netbox/users/migrations/0001_squashed_0011.py b/netbox/users/migrations/0001_squashed_0011.py index 5efbcaec8..cad84201c 100644 --- a/netbox/users/migrations/0001_squashed_0011.py +++ b/netbox/users/migrations/0001_squashed_0011.py @@ -4,6 +4,7 @@ import django.contrib.postgres.fields import django.core.validators from django.db import migrations, models import django.db.models.deletion +import users.models class Migration(migrations.Migration): @@ -31,6 +32,33 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('password', models.CharField(max_length=128)), + ('last_login', models.DateTimeField(blank=True, null=True)), + ('is_superuser', models.BooleanField(default=False)), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()])), + ('first_name', models.CharField(blank=True, max_length=150)), + ('last_name', models.CharField(blank=True, max_length=150)), + ('email', models.EmailField(blank=True, max_length=254)), + ('is_staff', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now)), + ('groups', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='auth.group')), + ('user_permissions', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='auth.permission')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'db_table': 'auth_user', + 'ordering': ('username',), + }, + managers=[ + ('objects', users.models.UserManager()), + ], + ), migrations.CreateModel( name='UserConfig', fields=[ diff --git a/netbox/users/migrations/0002_squashed_0004.py b/netbox/users/migrations/0002_squashed_0004.py index 367dfb7fc..078721c48 100644 --- a/netbox/users/migrations/0002_squashed_0004.py +++ b/netbox/users/migrations/0002_squashed_0004.py @@ -59,20 +59,4 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.GroupManager()), ], ), - migrations.CreateModel( - name='NetBoxUser', - fields=[ - ], - options={ - 'verbose_name': 'User', - 'proxy': True, - 'indexes': [], - 'constraints': [], - 'ordering': ('username',), - }, - bases=('auth.user',), - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), ] diff --git a/netbox/users/migrations/0005_alter_user_table.py b/netbox/users/migrations/0005_alter_user_table.py new file mode 100644 index 000000000..6c4a815dd --- /dev/null +++ b/netbox/users/migrations/0005_alter_user_table.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.1 on 2024-01-31 23:18 + +from django.db import migrations + + +def update_content_types(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + # Delete the new ContentTypes effected by the new models in the users app + ContentType.objects.filter(app_label='users', model='user').delete() + + # Update the app labels of the original ContentTypes for auth.User to ensure + # that any foreign key references are preserved + ContentType.objects.filter(app_label='auth', model='user').update(app_label='users') + + netboxuser_ct = ContentType.objects.filter(app_label='users', model='netboxuser').first() + if netboxuser_ct: + user_ct = ContentType.objects.filter(app_label='users', model='user').first() + CustomField = apps.get_model('extras', 'CustomField') + CustomField.objects.filter(object_type_id=netboxuser_ct.id).update(object_type_id=user_ct.id) + netboxuser_ct.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_squashed_0004'), + ] + + operations = [ + # 0001_squashed had model with db_table=auth_user - now we switch it + # to None to use the default Django resolution (users.user) + migrations.AlterModelTable( + name='user', + table=None, + ), + migrations.RunPython( + code=update_content_types, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 52ce55e6c..5e817be0b 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -2,7 +2,10 @@ import binascii import os from django.conf import settings -from django.contrib.auth.models import Group, GroupManager, User, UserManager +from django.contrib.auth import get_user_model +from django.contrib.auth.models import ( + AbstractUser, Group, GroupManager, User as DjangoUser, UserManager as DjangoUserManager +) from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator @@ -23,9 +26,9 @@ from .constants import * __all__ = ( 'NetBoxGroup', - 'NetBoxUser', 'ObjectPermission', 'Token', + 'User', 'UserConfig', ) @@ -34,7 +37,7 @@ __all__ = ( # Proxies for Django's User and Group models # -class NetBoxUserManager(UserManager.from_queryset(RestrictedQuerySet)): +class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)): pass @@ -42,20 +45,19 @@ class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)): pass -class NetBoxUser(User): +class User(AbstractUser): """ Proxy contrib.auth.models.User for the UI """ - objects = NetBoxUserManager() + objects = UserManager() class Meta: - proxy = True ordering = ('username',) verbose_name = _('user') verbose_name_plural = _('users') def get_absolute_url(self): - return reverse('users:netboxuser', args=[self.pk]) + return reverse('users:user', args=[self.pk]) def clean(self): super().clean() @@ -91,7 +93,7 @@ class UserConfig(models.Model): This model stores arbitrary user-specific preferences in a JSON data structure. """ user = models.OneToOneField( - to=User, + to=get_user_model(), on_delete=models.CASCADE, related_name='config' ) @@ -220,7 +222,6 @@ class UserConfig(models.Model): @receiver(post_save, sender=User) -@receiver(post_save, sender=NetBoxUser) def create_userconfig(instance, created, raw=False, **kwargs): """ Automatically create a new UserConfig when a new User is created. Skip this if importing a user from a fixture. @@ -240,7 +241,7 @@ class Token(models.Model): It also supports setting an expiration time and toggling write ability. """ user = models.ForeignKey( - to=User, + to=get_user_model(), on_delete=models.CASCADE, related_name='tokens' ) @@ -364,7 +365,7 @@ class ObjectPermission(models.Model): related_name='object_permissions' ) users = models.ManyToManyField( - to=User, + to=get_user_model(), blank=True, related_name='object_permissions' ) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index afb270568..781660817 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -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, NetBoxUser, ObjectPermission, Token +from users.models import NetBoxGroup, User, ObjectPermission, Token __all__ = ( 'GroupTable', @@ -49,7 +49,7 @@ class UserTable(NetBoxTable): ) class Meta(NetBoxTable.Meta): - model = NetBoxUser + model = User fields = ( 'pk', 'id', 'username', 'first_name', 'last_name', 'email', 'groups', 'is_active', 'is_staff', 'is_superuser', 'last_login', @@ -103,7 +103,7 @@ class ObjectPermissionTable(NetBoxTable): ) users = columns.ManyToManyColumn( verbose_name=_('Users'), - linkify_item=('users:netboxuser', {'pk': tables.A('pk')}) + linkify_item=('users:user', {'pk': tables.A('pk')}) ) groups = columns.ManyToManyColumn( verbose_name=_('Groups'), diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index c6408fc01..259b7a857 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -15,7 +15,7 @@ class UserTestCase( ViewTestCases.BulkEditObjectsViewTestCase, ViewTestCases.BulkDeleteObjectsViewTestCase, ): - model = NetBoxUser + model = User maxDiff = None validation_excluded_fields = ['password'] @@ -27,11 +27,11 @@ class UserTestCase( def setUpTestData(cls): users = ( - NetBoxUser(username='username1', first_name='first1', last_name='last1', email='user1@foo.com', password='pass1xxx'), - NetBoxUser(username='username2', first_name='first2', last_name='last2', email='user2@foo.com', password='pass2xxx'), - NetBoxUser(username='username3', first_name='first3', last_name='last3', email='user3@foo.com', password='pass3xxx'), + User(username='username1', first_name='first1', last_name='last1', email='user1@foo.com', password='pass1xxx'), + User(username='username2', first_name='first2', last_name='last2', email='user2@foo.com', password='pass2xxx'), + User(username='username3', first_name='first3', last_name='last3', email='user3@foo.com', password='pass3xxx'), ) - NetBoxUser.objects.bulk_create(users) + User.objects.bulk_create(users) cls.form_data = { 'username': 'usernamex', diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 210d8a2c7..486a0c771 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -15,12 +15,12 @@ urlpatterns = [ path('tokens//', include(get_model_urls('users', 'token'))), # Users - path('users/', views.UserListView.as_view(), name='netboxuser_list'), - path('users/add/', views.UserEditView.as_view(), name='netboxuser_add'), - path('users/edit/', views.UserBulkEditView.as_view(), name='netboxuser_bulk_edit'), - path('users/import/', views.UserBulkImportView.as_view(), name='netboxuser_import'), - path('users/delete/', views.UserBulkDeleteView.as_view(), name='netboxuser_bulk_delete'), - path('users//', include(get_model_urls('users', 'netboxuser'))), + path('users/', views.UserListView.as_view(), name='user_list'), + path('users/add/', views.UserEditView.as_view(), name='user_add'), + path('users/edit/', views.UserBulkEditView.as_view(), name='user_bulk_edit'), + path('users/import/', views.UserBulkImportView.as_view(), name='user_import'), + path('users/delete/', views.UserBulkDeleteView.as_view(), name='user_bulk_delete'), + path('users//', include(get_model_urls('users', 'user'))), # Groups path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 2e7a47c12..324125604 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -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, NetBoxUser, ObjectPermission, Token +from .models import NetBoxGroup, User, ObjectPermission, Token # @@ -56,15 +56,15 @@ class TokenBulkDeleteView(generic.BulkDeleteView): # class UserListView(generic.ObjectListView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() filterset = filtersets.UserFilterSet filterset_form = forms.UserFilterForm table = tables.UserTable -@register_model_view(NetBoxUser) +@register_model_view(User) class UserView(generic.ObjectView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() template_name = 'users/user.html' def get_extra_context(self, request, instance): @@ -76,31 +76,31 @@ class UserView(generic.ObjectView): } -@register_model_view(NetBoxUser, 'edit') +@register_model_view(User, 'edit') class UserEditView(generic.ObjectEditView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() form = forms.UserForm -@register_model_view(NetBoxUser, 'delete') +@register_model_view(User, 'delete') class UserDeleteView(generic.ObjectDeleteView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() class UserBulkEditView(generic.BulkEditView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() filterset = filtersets.UserFilterSet table = tables.UserTable form = forms.UserBulkEditForm class UserBulkImportView(generic.BulkImportView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() model_form = forms.UserImportForm class UserBulkDeleteView(generic.BulkDeleteView): - queryset = NetBoxUser.objects.all() + queryset = User.objects.all() filterset = filtersets.UserFilterSet table = tables.UserTable