diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 13faee4e2..35e411c12 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -194,6 +194,28 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description',
}
+ def test_get_object_with_only_site_view_permission_hides_unauthorized_embedded_panels(self):
+ site = self._get_queryset().first()
+
+ obj_perm = ObjectPermission(
+ name='Test permission',
+ actions=['view'],
+ )
+ obj_perm.save()
+ obj_perm.users.add(self.user)
+ obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
+
+ response = self.client.get(site.get_absolute_url())
+ self.assertHttpStatus(response, 200)
+
+ for panel, url in (
+ ('locations', reverse('dcim:location_list')),
+ ('devices', reverse('dcim:device_list')),
+ ('image attachments', reverse('extras:imageattachment_list')),
+ ):
+ with self.subTest(panel=panel):
+ self.assertNotContains(response, url)
+
class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Location
diff --git a/netbox/netbox/tests/test_ui.py b/netbox/netbox/tests/test_ui.py
index cb76517c1..8a328793f 100644
--- a/netbox/netbox/tests/test_ui.py
+++ b/netbox/netbox/tests/test_ui.py
@@ -1,4 +1,4 @@
-from django.test import TestCase
+from django.test import RequestFactory, TestCase
from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
from circuits.models import (
@@ -8,9 +8,12 @@ from circuits.models import (
VirtualCircuitTermination,
VirtualCircuitType,
)
+from core.models import ObjectType
from dcim.choices import InterfaceTypeChoices
-from dcim.models import Interface
+from dcim.models import Interface, Site
from netbox.ui import attrs
+from netbox.ui.panels import ObjectsTablePanel
+from users.models import ObjectPermission, User
from utilities.testing import create_test_device
from vpn.choices import (
AuthenticationAlgorithmChoices,
@@ -213,3 +216,55 @@ class RelatedObjectListAttrTest(TestCase):
self.assertInHTML('
IKE Proposal 2', rendered)
self.assertNotIn('IKE Proposal 3', rendered)
self.assertIn('…', rendered)
+
+
+class ObjectsTablePanelTest(TestCase):
+ """
+ Verify that ObjectsTablePanel.should_render() hides the panel when
+ the requesting user lacks view permission for the panel's model.
+ """
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = User.objects.create_user(username='test_user', password='test_password')
+
+ # Grant view permission only for Site
+ obj_perm = ObjectPermission.objects.create(
+ name='View sites only',
+ actions=['view'],
+ )
+ obj_perm.object_types.add(ObjectType.objects.get_for_model(Site))
+ obj_perm.users.add(cls.user)
+
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.panel = ObjectsTablePanel(model='dcim.site')
+ self.panel_no_perm = ObjectsTablePanel(model='dcim.location')
+
+ def _make_context(self, user=None):
+ if user is None:
+ return {}
+ request = self.factory.get('/')
+ request.user = user
+ return {'request': request}
+
+ def test_should_render_without_request(self):
+ """
+ Panel should render when no request is present in context.
+ """
+ context = self.panel.get_context({})
+ self.assertTrue(self.panel.should_render(context))
+
+ def test_should_render_with_permission(self):
+ """
+ Panel should render when the user has view permission for the panel's model.
+ """
+ context = self.panel.get_context(self._make_context(self.user))
+ self.assertTrue(self.panel.should_render(context))
+
+ def test_should_not_render_without_permission(self):
+ """
+ Panel should be hidden when the user lacks view permission for the panel's model.
+ """
+ context = self.panel_no_perm.get_context(self._make_context(self.user))
+ self.assertFalse(self.panel_no_perm.should_render(context))
diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py
index f58e98449..f1afa963b 100644
--- a/netbox/netbox/ui/panels.py
+++ b/netbox/netbox/ui/panels.py
@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs
from netbox.ui.actions import CopyContent
from utilities.data import resolve_attr_path
+from utilities.permissions import get_permission_for_model
from utilities.querydict import dict_to_querydict
from utilities.string import title
from utilities.templatetags.plugins import _get_registered_content
@@ -74,6 +75,15 @@ class Panel:
'panel_class': self.__class__.__name__,
}
+ def should_render(self, context):
+ """
+ Determines whether the panel should render on the page. (Default: True)
+
+ Parameters:
+ context (dict): The panel's prepared context (the return value of get_context())
+ """
+ return True
+
def render(self, context):
"""
Render the panel as HTML.
@@ -81,7 +91,10 @@ class Panel:
Parameters:
context (dict): The template context
"""
- return render_to_string(self.template_name, self.get_context(context))
+ ctx = self.get_context(context)
+ if not self.should_render(ctx):
+ return ''
+ return render_to_string(self.template_name, ctx, request=ctx.get('request'))
#
@@ -314,6 +327,16 @@ class ObjectsTablePanel(Panel):
'url_params': dict_to_querydict(url_params),
}
+ def should_render(self, context):
+ """
+ Hide the panel if the user does not have view permission for the panel's model.
+ """
+ request = context.get('request')
+ if request is None:
+ return True
+
+ return request.user.has_perm(get_permission_for_model(self.model, 'view'))
+
class TemplatePanel(Panel):
"""