Filter custom links by the current user's view permissions before rendering them on object detail views and table columns.
This commit is contained in:
parent
83439ba00f
commit
626e1ef1f8
|
|
@ -33,8 +33,9 @@ def custom_links(context, obj):
|
|||
"""
|
||||
Render all applicable links for the given object.
|
||||
"""
|
||||
user = context['request'].user
|
||||
object_type = ObjectType.objects.get_for_model(obj)
|
||||
custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True)
|
||||
custom_links = CustomLink.objects.restrict(user, 'view').filter(object_types=object_type, enabled=True)
|
||||
if not custom_links:
|
||||
return ''
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.context_processors import PermWrapper
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Site
|
||||
from extras.models import CustomLink
|
||||
from extras.templatetags.custom_links import custom_links
|
||||
from users.models import ObjectPermission
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomLinkTemplateTagTest(TestCase):
|
||||
"""
|
||||
The custom_links template tag must honor object-level permissions on CustomLink (see #22439).
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
cls.custom_link = CustomLink.objects.create(
|
||||
name='Custom Link 1',
|
||||
enabled=True,
|
||||
weight=100,
|
||||
new_window=False,
|
||||
link_text='Link 1',
|
||||
link_url='http://example.com/'
|
||||
)
|
||||
cls.custom_link.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
def render(self, user):
|
||||
request = RequestFactory().get('/')
|
||||
request.user = user
|
||||
context = {
|
||||
'request': request,
|
||||
'user': user,
|
||||
'perms': PermWrapper(user),
|
||||
}
|
||||
return custom_links(context, self.site)
|
||||
|
||||
def test_no_permission_hides_link(self):
|
||||
# A user without the view_customlink permission must not see the link.
|
||||
user = User.objects.create_user(username='user1')
|
||||
self.assertNotIn('Link 1', self.render(user))
|
||||
|
||||
def test_constrained_permission_hides_link(self):
|
||||
# A user granted view_customlink via a constraint that excludes the link must not see it (#22439).
|
||||
user = User.objects.create_user(username='user2')
|
||||
permission = ObjectPermission.objects.create(
|
||||
name='Constrained custom links',
|
||||
actions=['view'],
|
||||
constraints={'name': 'Some Other Link'} # Does not match Custom Link 1
|
||||
)
|
||||
permission.object_types.set([ObjectType.objects.get_for_model(CustomLink)])
|
||||
permission.users.set([user])
|
||||
|
||||
# Re-fetch to clear any cached permissions
|
||||
user = User.objects.get(pk=user.pk)
|
||||
self.assertNotIn('Link 1', self.render(user))
|
||||
|
||||
def test_permitted_link_is_shown(self):
|
||||
# A user granted view_customlink covering the link must see it.
|
||||
user = User.objects.create_user(username='user3')
|
||||
permission = ObjectPermission.objects.create(
|
||||
name='Custom links',
|
||||
actions=['view'],
|
||||
constraints={'name': 'Custom Link 1'} # Matches Custom Link 1
|
||||
)
|
||||
permission.object_types.set([ObjectType.objects.get_for_model(CustomLink)])
|
||||
permission.users.set([user])
|
||||
|
||||
# Re-fetch to clear any cached permissions
|
||||
user = User.objects.get(pk=user.pk)
|
||||
self.assertIn('Link 1', self.render(user))
|
||||
|
|
@ -184,7 +184,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
|
||||
|
||||
class CustomLinkRenderingTestCase(TestCase):
|
||||
user_permissions = ['dcim.view_site']
|
||||
user_permissions = ['dcim.view_site', 'extras.view_customlink']
|
||||
|
||||
def test_view_object_with_custom_link(self):
|
||||
customlink = CustomLink(
|
||||
|
|
@ -203,6 +203,47 @@ class CustomLinkRenderingTestCase(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(f'FOO {site.name} BAR', str(response.content))
|
||||
|
||||
def test_list_view_custom_link_column(self):
|
||||
# A custom link column must render in the list view when the user can view the CustomLink.
|
||||
customlink = CustomLink(
|
||||
name='Test',
|
||||
link_text='FOO {{ object.name }} BAR',
|
||||
link_url='http://example.com/?site={{ object.slug }}',
|
||||
new_window=False
|
||||
)
|
||||
customlink.save()
|
||||
customlink.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
site = Site(name='Test Site', slug='test-site')
|
||||
site.save()
|
||||
|
||||
# Custom link columns are hidden by default; explicitly include the column
|
||||
response = self.client.get(f"{reverse('dcim:site_list')}?include_columns=cl_Test")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(f'FOO {site.name} BAR', str(response.content))
|
||||
|
||||
def test_list_view_custom_link_column_hidden_without_permission(self):
|
||||
# A custom link column must be excluded from the list view when the user cannot view the
|
||||
# CustomLink, even if explicitly requested (#22439).
|
||||
customlink = CustomLink(
|
||||
name='Test',
|
||||
link_text='FOO {{ object.name }} BAR',
|
||||
link_url='http://example.com/?site={{ object.slug }}',
|
||||
new_window=False
|
||||
)
|
||||
customlink.save()
|
||||
customlink.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
site = Site(name='Test Site', slug='test-site')
|
||||
site.save()
|
||||
|
||||
# Revoke permission to view custom links
|
||||
self.remove_permissions('extras.view_customlink')
|
||||
|
||||
response = self.client.get(f"{reverse('dcim:site_list')}?include_columns=cl_Test")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(f'FOO {site.name} BAR', str(response.content))
|
||||
|
||||
|
||||
class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = SavedFilter
|
||||
|
|
|
|||
|
|
@ -287,6 +287,38 @@ class NetBoxTable(BaseTable):
|
|||
|
||||
super().__init__(*args, extra_columns=extra_columns, **kwargs)
|
||||
|
||||
def configure(self, request):
|
||||
# Remove custom link columns referencing CustomLinks the user cannot view (#22439).
|
||||
# These columns are added for all enabled CustomLinks in __init__(), before the request
|
||||
# (and thus the user) is known, so object-level permissions are enforced here instead.
|
||||
self._restrict_customlink_columns(request.user)
|
||||
|
||||
super().configure(request)
|
||||
|
||||
def _restrict_customlink_columns(self, user):
|
||||
"""
|
||||
Exclude any custom link columns which reference a CustomLink the user does not have
|
||||
permission to view.
|
||||
"""
|
||||
customlinks = {
|
||||
name: column.column.customlink
|
||||
for name, column in self.columns.iteritems()
|
||||
if isinstance(column.column, columns.CustomLinkColumn)
|
||||
}
|
||||
if not customlinks:
|
||||
return
|
||||
|
||||
permitted = set(
|
||||
CustomLink.objects.restrict(user, 'view').filter(
|
||||
pk__in=[cl.pk for cl in customlinks.values()]
|
||||
).values_list('pk', flat=True)
|
||||
)
|
||||
excluded = tuple(
|
||||
name for name, customlink in customlinks.items() if customlink.pk not in permitted
|
||||
)
|
||||
if excluded:
|
||||
self.exclude = (*self.exclude, *excluded)
|
||||
|
||||
@cached_property
|
||||
def htmx_url(self):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue