diff --git a/docs/administration/authentication/overview.md b/docs/administration/authentication/overview.md index 19c86a4c0..6b805ee92 100644 --- a/docs/administration/authentication/overview.md +++ b/docs/administration/authentication/overview.md @@ -2,7 +2,7 @@ ## Local Authentication -Local user accounts and groups can be created in NetBox under the "Authentication" section in the "Admin" menu. This section is available only to users with the "staff" permission enabled. +Local user accounts and groups can be created in NetBox under the "Authentication" section in the "Admin" menu. At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to individual users and/or groups as needed. diff --git a/docs/configuration/remote-authentication.md b/docs/configuration/remote-authentication.md index 5d5f1ee58..2dbb689c2 100644 --- a/docs/configuration/remote-authentication.md +++ b/docs/configuration/remote-authentication.md @@ -127,19 +127,3 @@ The list of groups that promote an remote User to Superuser on Login. If group i Default: `[]` (Empty list) The list of users that get promoted to Superuser on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) - ---- - -## REMOTE_AUTH_STAFF_GROUPS - -Default: `[]` (Empty list) - -The list of groups that promote an remote User to Staff on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) - ---- - -## REMOTE_AUTH_STAFF_USERS - -Default: `[]` (Empty list) - -The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index 7de9f116d..4d27f2f01 100644 --- a/docs/installation/6-ldap.md +++ b/docs/installation/6-ldap.md @@ -121,7 +121,6 @@ AUTH_LDAP_MIRROR_GROUPS = True # Define special user types using groups. Exercise great caution when assigning superuser status. AUTH_LDAP_USER_FLAGS_BY_GROUP = { "is_active": "cn=active,ou=groups,dc=example,dc=com", - "is_staff": "cn=staff,ou=groups,dc=example,dc=com", "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com" } @@ -134,7 +133,6 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600 ``` * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. -* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions. * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions. !!! warning @@ -248,7 +246,6 @@ AUTH_LDAP_MIRROR_GROUPS = True # Define special user types using groups. Exercise great caution when assigning superuser status. AUTH_LDAP_USER_FLAGS_BY_GROUP = { "is_active": "cn=active,ou=groups,dc=example,dc=com", - "is_staff": "cn=staff,ou=groups,dc=example,dc=com", "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com" } diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md index b5e2694b4..bb855bcea 100644 --- a/docs/plugins/development/navigation.md +++ b/docs/plugins/development/navigation.md @@ -64,14 +64,17 @@ item1 = PluginMenuItem( A `PluginMenuItem` has the following attributes: -| Attribute | Required | Description | -|-----------------|----------|----------------------------------------------------------------------------------------------------------| -| `link` | Yes | Name of the URL path to which this menu item links | -| `link_text` | Yes | The text presented to the user | -| `permissions` | - | A list of permissions required to display this link | -| `auth_required` | - | Display only for authenticated users | -| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) | -| `buttons` | - | An iterable of PluginMenuButton instances to include | +| Attribute | Required | Description | +|-----------------|----------|------------------------------------------------------| +| `link` | Yes | Name of the URL path to which this menu item links | +| `link_text` | Yes | The text presented to the user | +| `permissions` | - | A list of permissions required to display this link | +| `auth_required` | - | Display only for authenticated users | +| `staff_only` | - | Display only for superusers | +| `buttons` | - | An iterable of PluginMenuButton instances to include | + +!!! note "Changed in NetBox v4.5" + In releases prior to NetBox v4.5, `staff_only` restricted display of a menu item to only users with `is_staff` set to True. In NetBox v4.5, the `is_staff` flag was removed from the user model. Menu items with `staff_only` set to True are now displayed only for superusers. ## Menu Buttons diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py index e9569a717..afa30ce56 100644 --- a/netbox/core/api/views.py +++ b/netbox/core/api/views.py @@ -9,7 +9,6 @@ from drf_spectacular.utils import extend_schema from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied -from rest_framework.permissions import IsAdminUser from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.viewsets import ReadOnlyModelViewSet @@ -24,6 +23,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import LimitOffsetListPagination from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet +from utilities.api import IsSuperuser from . import serializers @@ -99,7 +99,7 @@ class BaseRQViewSet(viewsets.ViewSet): """ Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data(). """ - permission_classes = [IsAdminUser] + permission_classes = [IsSuperuser] serializer_class = None def get_data(self): diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py index 4a285bdb4..159a8e5e1 100644 --- a/netbox/core/tests/test_api.py +++ b/netbox/core/tests/test_api.py @@ -134,10 +134,7 @@ class BackgroundTaskTestCase(TestCase): Create a user and token for API calls. """ # Create the test user and assign permissions - self.user = User.objects.create_user(username='testuser') - self.user.is_staff = True - self.user.is_active = True - self.user.save() + self.user = User.objects.create_user(username='testuser', is_active=True) self.token = Token.objects.create(user=self.user) self.header = {'HTTP_AUTHORIZATION': f'Token {self.token.key}'} @@ -150,13 +147,11 @@ class BackgroundTaskTestCase(TestCase): url = reverse('core-api:rqqueue-list') # Attempt to load view without permission - self.user.is_staff = False - self.user.save() response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 403) # Load view with permission - self.user.is_staff = True + self.user.is_superuser = True self.user.save() response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) @@ -165,7 +160,16 @@ class BackgroundTaskTestCase(TestCase): self.assertIn('low', str(response.content)) def test_background_queue(self): - response = self.client.get(reverse('core-api:rqqueue-detail', args=['default']), **self.header) + url = reverse('core-api:rqqueue-detail', args=['default']) + + # Attempt to load view without permission + response = self.client.get(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Load view with permission + self.user.is_superuser = True + self.user.save() + response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) self.assertIn('default', str(response.content)) self.assertIn('oldest_job_timestamp', str(response.content)) @@ -174,8 +178,16 @@ class BackgroundTaskTestCase(TestCase): def test_background_task_list(self): queue = get_queue('default') queue.enqueue(self.dummy_job_default) + url = reverse('core-api:rqtask-list') - response = self.client.get(reverse('core-api:rqtask-list'), **self.header) + # Attempt to load view without permission + response = self.client.get(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Load view with permission + self.user.is_superuser = True + self.user.save() + response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) self.assertIn('origin', str(response.content)) self.assertIn('core.tests.test_api.BackgroundTaskTestCase.dummy_job_default()', str(response.content)) @@ -183,8 +195,16 @@ class BackgroundTaskTestCase(TestCase): def test_background_task(self): queue = get_queue('default') job = queue.enqueue(self.dummy_job_default) + url = reverse('core-api:rqtask-detail', args=[job.id]) - response = self.client.get(reverse('core-api:rqtask-detail', args=[job.id]), **self.header) + # Attempt to load view without permission + response = self.client.get(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Load view with permission + self.user.is_superuser = True + self.user.save() + response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) self.assertIn(str(job.id), str(response.content)) self.assertIn('origin', str(response.content)) @@ -194,45 +214,65 @@ class BackgroundTaskTestCase(TestCase): def test_background_task_delete(self): queue = get_queue('default') job = queue.enqueue(self.dummy_job_default) + url = reverse('core-api:rqtask-delete', args=[job.id]) - response = self.client.post(reverse('core-api:rqtask-delete', args=[job.id]), **self.header) + # Attempt to load view without permission + response = self.client.get(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Load view with permission + self.user.is_superuser = True + self.user.save() + response = self.client.post(url, **self.header) self.assertEqual(response.status_code, 200) self.assertFalse(RQ_Job.exists(job.id, connection=queue.connection)) queue = get_queue('default') self.assertNotIn(job.id, queue.job_ids) def test_background_task_requeue(self): - queue = get_queue('default') - # Enqueue & run a job that will fail + queue = get_queue('default') job = queue.enqueue(self.dummy_job_failing) worker = get_worker('default') with disable_logging(): worker.work(burst=True) self.assertTrue(job.is_failed) + url = reverse('core-api:rqtask-requeue', args=[job.id]) + + # Attempt to requeue the job without permission + response = self.client.post(url, **self.header) + self.assertEqual(response.status_code, 403) # Re-enqueue the failed job and check that its status has been reset - response = self.client.post(reverse('core-api:rqtask-requeue', args=[job.id]), **self.header) + self.user.is_superuser = True + self.user.save() + response = self.client.post(url, **self.header) self.assertEqual(response.status_code, 200) job = RQ_Job.fetch(job.id, queue.connection) self.assertFalse(job.is_failed) def test_background_task_enqueue(self): - queue = get_queue('default') - # Enqueue some jobs that each depends on its predecessor + queue = get_queue('default') job = previous_job = None for _ in range(0, 3): job = queue.enqueue(self.dummy_job_default, depends_on=previous_job) previous_job = job + url = reverse('core-api:rqtask-enqueue', args=[job.id]) # Check that the last job to be enqueued has a status of deferred self.assertIsNotNone(job) self.assertEqual(job.get_status(), JobStatus.DEFERRED) self.assertIsNone(job.enqueued_at) + # Attempt to force-enqueue the job without permission + response = self.client.post(url, **self.header) + self.assertEqual(response.status_code, 403) + # Force-enqueue the deferred job - response = self.client.post(reverse('core-api:rqtask-enqueue', args=[job.id]), **self.header) + self.user.is_superuser = True + self.user.save() + response = self.client.post(url, **self.header) self.assertEqual(response.status_code, 200) # Check that job's status is updated correctly @@ -242,19 +282,27 @@ class BackgroundTaskTestCase(TestCase): def test_background_task_stop(self): queue = get_queue('default') - worker = get_worker('default') job = queue.enqueue(self.dummy_job_default) worker.prepare_job_execution(job) - + url = reverse('core-api:rqtask-stop', args=[job.id]) self.assertEqual(job.get_status(), JobStatus.STARTED) - response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header) + + # Attempt to stop the task without permission + response = self.client.post(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Stop the task + self.user.is_superuser = True + self.user.save() + response = self.client.post(url, **self.header) self.assertEqual(response.status_code, 200) with disable_logging(): worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection) self.assertEqual(len(started_job_registry), 0) + # Verify that the task was cancelled canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection) self.assertEqual(len(canceled_job_registry), 1) self.assertIn(job.id, canceled_job_registry) @@ -262,19 +310,34 @@ class BackgroundTaskTestCase(TestCase): def test_worker_list(self): worker1 = get_worker('default', name=uuid.uuid4().hex) worker1.register_birth() - worker2 = get_worker('high') worker2.register_birth() + url = reverse('core-api:rqworker-list') - response = self.client.get(reverse('core-api:rqworker-list'), **self.header) + # Attempt to fetch the worker list without permission + response = self.client.get(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Fetch the worker list + self.user.is_superuser = True + self.user.save() + response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) self.assertIn(str(worker1.name), str(response.content)) def test_worker(self): worker1 = get_worker('default', name=uuid.uuid4().hex) worker1.register_birth() + url = reverse('core-api:rqworker-detail', args=[worker1.name]) - response = self.client.get(reverse('core-api:rqworker-detail', args=[worker1.name]), **self.header) + # Attempt to fetch a worker without permission + response = self.client.get(url, **self.header) + self.assertEqual(response.status_code, 403) + + # Fetch the worker + self.user.is_superuser = True + self.user.save() + response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) self.assertIn(str(worker1.name), str(response.content)) self.assertIn('birth_date', str(response.content)) diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py index 1001243eb..2f0ef59e6 100644 --- a/netbox/core/tests/test_views.py +++ b/netbox/core/tests/test_views.py @@ -158,7 +158,7 @@ class BackgroundTaskTestCase(TestCase): def setUp(self): super().setUp() - self.user.is_staff = True + self.user.is_superuser = True self.user.is_active = True self.user.save() @@ -171,13 +171,13 @@ class BackgroundTaskTestCase(TestCase): url = reverse('core:background_queue_list') # Attempt to load view without permission - self.user.is_staff = False + self.user.is_superuser = False self.user.save() response = self.client.get(url) self.assertEqual(response.status_code, 403) # Load view with permission - self.user.is_staff = True + self.user.is_superuser = True self.user.save() response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -356,7 +356,7 @@ class SystemTestCase(TestCase): def setUp(self): super().setUp() - self.user.is_staff = True + self.user.is_superuser = True self.user.save() def test_system_view_default(self): diff --git a/netbox/core/views.py b/netbox/core/views.py index b18937308..aa3be7303 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -366,7 +366,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View): class BaseRQView(UserPassesTestMixin, View): def test_func(self): - return self.request.user.is_staff + return self.request.user.is_superuser class BackgroundQueueListView(TableMixin, BaseRQView): @@ -549,7 +549,7 @@ class WorkerView(BaseRQView): class SystemView(UserPassesTestMixin, View): def test_func(self): - return self.request.user.is_staff + return self.request.user.is_superuser def get(self, request): @@ -632,7 +632,7 @@ class BasePluginView(UserPassesTestMixin, View): CACHE_KEY_CATALOG_ERROR = 'plugins-catalog-error' def test_func(self): - return self.request.user.is_staff + return self.request.user.is_superuser def get_cached_plugins(self, request): catalog_plugins = {} diff --git a/netbox/netbox/authentication/__init__.py b/netbox/netbox/authentication/__init__.py index 25f9b902c..8596c59ab 100644 --- a/netbox/netbox/authentication/__init__.py +++ b/netbox/netbox/authentication/__init__.py @@ -184,14 +184,13 @@ class RemoteUserBackend(_RemoteUserBackend): else: user.groups.clear() logger.debug(f"Stripping user {user} from Groups") + + # Evaluate superuser status user.is_superuser = self._is_superuser(user) logger.debug(f"User {user} is Superuser: {user.is_superuser}") logger.debug( f"User {user} should be Superuser: {self._is_superuser(user)}") - user.is_staff = self._is_staff(user) - logger.debug(f"User {user} is Staff: {user.is_staff}") - logger.debug(f"User {user} should be Staff: {self._is_staff(user)}") user.save() return user @@ -251,19 +250,8 @@ class RemoteUserBackend(_RemoteUserBackend): return bool(result) def _is_staff(self, user): - logger = logging.getLogger('netbox.auth.RemoteUserBackend') - staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS - logger.debug(f"Superuser Groups: {staff_groups}") - staff_users = settings.REMOTE_AUTH_STAFF_USERS - logger.debug(f"Staff Users :{staff_users}") - user_groups = set() - for g in user.groups.all(): - user_groups.add(g.name) - logger.debug(f"User {user.username} is in Groups:{user_groups}") - result = user.username in staff_users or ( - set(user_groups) & set(staff_groups)) - logger.debug(f"User {user.username} in Staff Users :{result}") - return bool(result) + # Retain for pre-v4.5 compatibility + return user.is_superuser def configure_user(self, request, user): logger = logging.getLogger('netbox.auth.RemoteUserBackend') diff --git a/netbox/netbox/plugins/views.py b/netbox/netbox/plugins/views.py index feee78e82..f413b8032 100644 --- a/netbox/netbox/plugins/views.py +++ b/netbox/netbox/plugins/views.py @@ -3,12 +3,12 @@ from collections import OrderedDict from django.apps import apps from django.urls.exceptions import NoReverseMatch from drf_spectacular.utils import extend_schema -from rest_framework import permissions from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView from netbox.registry import registry +from utilities.api import IsSuperuser @extend_schema(exclude=True) @@ -16,7 +16,7 @@ class InstalledPluginsAPIView(APIView): """ API view for listing all installed plugins """ - permission_classes = [permissions.IsAdminUser] + permission_classes = [IsSuperuser] _ignore_model_permissions = True schema = None diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 3bf453ca3..a0a6225c3 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -161,8 +161,6 @@ REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', []) REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL') REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME') REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME') -REMOTE_AUTH_STAFF_GROUPS = getattr(configuration, 'REMOTE_AUTH_STAFF_GROUPS', []) -REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', []) # Required by extras/migrations/0109_script_models.py REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index ab995db66..4537f14c9 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -47,9 +47,9 @@ class HomeView(ConditionalLoginRequiredMixin, View): )) dashboard = get_default_dashboard(config=DEFAULT_DASHBOARD).get_layout() - # Check whether a new release is available. (Only for staff/superusers.) + # Check whether a new release is available. (Only for superusers.) new_release = None - if request.user.is_staff or request.user.is_superuser: + if request.user.is_superuser: latest_release = cache.get('latest_release') if latest_release: release_version, release_url = latest_release diff --git a/netbox/templates/account/profile.html b/netbox/templates/account/profile.html index 442cce9ba..8ef0db6e8 100644 --- a/netbox/templates/account/profile.html +++ b/netbox/templates/account/profile.html @@ -39,10 +39,6 @@ {% trans "Superuser" %} {% checkmark request.user.is_superuser %} - - {% trans "Staff" %} - {% checkmark request.user.is_staff %} - diff --git a/netbox/templates/inc/user_menu.html b/netbox/templates/inc/user_menu.html index 4cca0b57e..50173ea56 100644 --- a/netbox/templates/inc/user_menu.html +++ b/netbox/templates/inc/user_menu.html @@ -27,8 +27,6 @@
{% if request.user.is_superuser %} {% trans "Admin" %} - {% elif request.user.is_staff %} - {% trans "Staff" %} {% else %} {% trans "User" %} {% endif %} diff --git a/netbox/templates/media_failure.html b/netbox/templates/media_failure.html index 8f357aea1..50ee1b63f 100644 --- a/netbox/templates/media_failure.html +++ b/netbox/templates/media_failure.html @@ -37,7 +37,7 @@ path. Refer to the installation documentation for further guidance. {% endblocktrans %}