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): """