Repository: netbox-community/netbox

Issue:
Remove the Django admin UI
### Proposed Changes

Disable the Django admin UI entirely. The few functions it still performs currently will be migrated to dedicated views within the primary UI.

### Justification

All aspects of NetBox should be accessible via its primary user interface. Disabling the standalone Django admin UI will allow us to deliver a single, cohesive interface, while eliminating dependencies on `django.contrib.admin`.

### Impact

The following admin views will be migrated to the primary UI (some have already been moved):

- ~Users~
- ~Groups~
- ~Config revisions~
- ~Permissions~
- ~Tokens~
- Background tasks
- Installed plugins

The following admin views will be removed entirely:

- Views registered by `python-social-auth`
- Any _admin_ views registered by plugins


Discussion:
> deliver a single, cohesive interface

Speaking of it. There is some rework with constrains needed, so API and saved filters paths will have same naming rules, as new Admin panel.

For example:
IP address have ?tenant_id= and ?tenant= filters, but "tenant_id"= and "tenant__slug"= in admin panel.
@stavr666 - Please create a seperate issue. Also do note that you are comparing filterset filters with queryset filters, which are not the same. This is not going to change. If you are missing a filterset filter, a FR is the right way to request it.
It's not a concern but https://github.com/Onemind-Services-LLC/netbox-secrets/ will need to redesign the UIs to allow for this change. Currently, an admin activates the user key from the admin panel and this list of keys is also only visible on the admin side as the `UserKey` model does not inherit most of the common functionality of NetBox. Atleast for this plugin, apart from moving the UI to tabler it'll basically be a complete UI overhaul along with the core

Resolving patch:
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -115,6 +115,7 @@
     'users.delete_token': ({'user': '$user'},),
 })
 DEVELOPER = getattr(configuration, 'DEVELOPER', False)
+DJANGO_ADMIN_ENABLED = getattr(configuration, 'DJANGO_ADMIN_ENABLED', False)
 DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
 EMAIL = getattr(configuration, 'EMAIL', {})
 EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', (
@@ -355,7 +356,6 @@ def _setting(name, default=None):
 #
 
 INSTALLED_APPS = [
-    'django.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
@@ -393,6 +393,9 @@ def _setting(name, default=None):
     'drf_spectacular_sidecar',
 ]
 
+if DJANGO_ADMIN_ENABLED:
+    INSTALLED_APPS.insert(0, 'django.contrib.admin')
+
 # Middleware
 MIDDLEWARE = [
     'graphiql_debug_toolbar.middleware.DebugToolbarMiddleware',
diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py
--- a/netbox/netbox/urls.py
+++ b/netbox/netbox/urls.py
@@ -11,7 +11,6 @@
 from netbox.graphql.views import GraphQLView
 from netbox.plugins.urls import plugin_patterns, plugin_api_patterns
 from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
-from .admin import admin_site
 
 _patterns = [
 
@@ -70,26 +69,25 @@
     # Plugins
     path('plugins/', include((plugin_patterns, 'plugins'))),
     path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))),
-
-    # Admin
-    path('admin/', admin_site.urls),
 ]
 
+# Django admin UI
+if settings.DJANGO_ADMIN_ENABLED:
+    from .admin import admin_site
+    _patterns.append(path('admin/', admin_site.urls))
 
+# django-debug-toolbar
 if settings.DEBUG:
     import debug_toolbar
-    _patterns += [
-        path('__debug__/', include(debug_toolbar.urls)),
-    ]
+    _patterns.append(path('__debug__/', include(debug_toolbar.urls)))
 
+# Prometheus metrics
 if settings.METRICS_ENABLED:
-    _patterns += [
-        path('', include('django_prometheus.urls')),
-    ]
+    _patterns.append(path('', include('django_prometheus.urls')))
 
 # Prepend BASE_PATH
 urlpatterns = [
-    path('{}'.format(settings.BASE_PATH), include(_patterns))
+    path(settings.BASE_PATH, include(_patterns))
 ]
 
 handler404 = 'netbox.views.errors.handler_404'


Test patch:
diff --git a/netbox/netbox/configuration_testing.py b/netbox/netbox/configuration_testing.py
--- a/netbox/netbox/configuration_testing.py
+++ b/netbox/netbox/configuration_testing.py
@@ -39,6 +39,8 @@
 
 SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
 
+DJANGO_ADMIN_ENABLED = True
+
 DEFAULT_PERMISSIONS = {}
 
 LOGGING = {
diff --git a/netbox/netbox/tests/dummy_plugin/navigation.py b/netbox/netbox/tests/dummy_plugin/navigation.py
--- a/netbox/netbox/tests/dummy_plugin/navigation.py
+++ b/netbox/netbox/tests/dummy_plugin/navigation.py
@@ -4,23 +4,23 @@
 
 items = (
     PluginMenuItem(
-        link='plugins:dummy_plugin:dummy_models',
+        link='plugins:dummy_plugin:dummy_model_list',
         link_text='Item 1',
         buttons=(
             PluginMenuButton(
-                link='admin:dummy_plugin_dummymodel_add',
-                title='Add a new dummy model',
+                link='plugins:dummy_plugin:dummy_model_add',
+                title='Button 1',
                 icon_class='mdi mdi-plus-thick',
             ),
             PluginMenuButton(
-                link='admin:dummy_plugin_dummymodel_add',
-                title='Add a new dummy model',
+                link='plugins:dummy_plugin:dummy_model_add',
+                title='Button 2',
                 icon_class='mdi mdi-plus-thick',
             ),
         )
     ),
     PluginMenuItem(
-        link='plugins:dummy_plugin:dummy_models',
+        link='plugins:dummy_plugin:dummy_model_list',
         link_text='Item 2',
     ),
 )
diff --git a/netbox/netbox/tests/dummy_plugin/urls.py b/netbox/netbox/tests/dummy_plugin/urls.py
--- a/netbox/netbox/tests/dummy_plugin/urls.py
+++ b/netbox/netbox/tests/dummy_plugin/urls.py
@@ -4,5 +4,6 @@
 
 
 urlpatterns = (
-    path('models/', views.DummyModelsView.as_view(), name='dummy_models'),
+    path('models/', views.DummyModelsView.as_view(), name='dummy_model_list'),
+    path('models/add/', views.DummyModelAddView.as_view(), name='dummy_model_add'),
 )
diff --git a/netbox/netbox/tests/dummy_plugin/views.py b/netbox/netbox/tests/dummy_plugin/views.py
--- a/netbox/netbox/tests/dummy_plugin/views.py
+++ b/netbox/netbox/tests/dummy_plugin/views.py
@@ -1,3 +1,6 @@
+import random
+import string
+
 from django.http import HttpResponse
 from django.views.generic import View
 
@@ -15,6 +18,20 @@ def get(self, request):
         return HttpResponse(f"Instances: {instance_count}")
 
 
+class DummyModelAddView(View):
+
+    def get(self, request):
+        return HttpResponse(f"Create an instance")
+
+    def post(self, request):
+        instance = DummyModel(
+            name=''.join(random.choices(string.ascii_lowercase, k=8)),
+            number=random.randint(1, 100000)
+        )
+        instance.save()
+        return HttpResponse(f"Instance created")
+
+
 @register_model_view(Site, 'extra', path='other-stuff')
 class ExtraCoreModelView(View):
 
diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py
--- a/netbox/netbox/tests/test_plugins.py
+++ b/netbox/netbox/tests/test_plugins.py
@@ -41,7 +41,7 @@ def test_admin(self):
     def test_views(self):
 
         # Test URL resolution
-        url = reverse('plugins:dummy_plugin:dummy_models')
+        url = reverse('plugins:dummy_plugin:dummy_model_list')
         self.assertEqual(url, '/plugins/dummy-plugin/models/')
 
         # Test GET request