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 @@
{% 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 @@
{% 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