mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-29 00:16:40 +00:00
User permissions check for Attachment API (#12689)
This commit is contained in:
@@ -789,6 +789,26 @@ class AttachmentFilter(FilterSet):
|
||||
tag_name = common.filters.TagsFilter()
|
||||
|
||||
|
||||
def get_viewable_attachment_model_types(user) -> set:
|
||||
"""Return the set of attachment 'model_type' labels the user has 'view' permission for.
|
||||
|
||||
Attachments are a generic table keyed by (model_type, model_id), with no RuleSet
|
||||
mapping of their own - so read access is based on the *linked* model's own
|
||||
RuleSet permission instead, mirroring how AttachmentDetail already checks
|
||||
'change'/'delete' via Attachment.check_permission() for writes.
|
||||
"""
|
||||
from common.validators import attachment_model_types
|
||||
from users.permissions import check_user_permission, prefetch_rule_sets
|
||||
|
||||
groups = prefetch_rule_sets(user)
|
||||
|
||||
return {
|
||||
model.__name__.lower()
|
||||
for model in attachment_model_types()
|
||||
if check_user_permission(user, model, 'view', groups=groups)
|
||||
}
|
||||
|
||||
|
||||
class AttachmentMixin:
|
||||
"""Mixin class for Attachment views."""
|
||||
|
||||
@@ -806,6 +826,14 @@ class AttachmentList(AttachmentMixin, BulkDeleteMixin, ListCreateAPI):
|
||||
ordering_fields = ['model_id', 'model_type', 'upload_date', 'file_size']
|
||||
search_fields = ['comment', 'model_id', 'model_type']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Restrict the queryset to attachments linked to a model the user can view."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
allowed_types = get_viewable_attachment_model_types(self.request.user)
|
||||
|
||||
return queryset.filter(model_type__in=allowed_types)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Save the user information when a file is uploaded."""
|
||||
serializer.save(upload_user=self.request.user)
|
||||
@@ -832,6 +860,17 @@ class AttachmentList(AttachmentMixin, BulkDeleteMixin, ListCreateAPI):
|
||||
class AttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
|
||||
"""Detail API endpoint for Attachment objects."""
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Retrieve a single attachment object, if the user has view permission."""
|
||||
attachment = self.get_object()
|
||||
|
||||
if not attachment.check_permission('view', request.user):
|
||||
raise PermissionDenied(
|
||||
_('User does not have permission to view this attachment')
|
||||
)
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""Update an existing attachment object."""
|
||||
attachment = self.get_object()
|
||||
|
||||
@@ -1062,6 +1062,79 @@ class AttachmentAPITests(InvenTreeAPITestCase):
|
||||
# Ensure that the file associated with each attachment has been removed
|
||||
self.assertFalse(default_storage.exists(att.attachment.path))
|
||||
|
||||
def test_attachment_read_permissions(self):
|
||||
"""Test that reading attachments is gated on the linked model's own view permission.
|
||||
|
||||
A user should not be able to list or retrieve attachments linked to a model
|
||||
type they have no view permission for, even though attachments themselves
|
||||
have no RuleSet of their own (see users.ruleset.get_ruleset_ignore).
|
||||
"""
|
||||
from common.models import Attachment
|
||||
from part.models import Part
|
||||
from stock.models import StockItem
|
||||
|
||||
part = Part.objects.create(name='Attachable Part', description='A part')
|
||||
item = StockItem.objects.create(part=part, quantity=10)
|
||||
|
||||
part_attachment = Attachment.objects.create(
|
||||
model_type='part',
|
||||
model_id=part.pk,
|
||||
comment='part attachment',
|
||||
link='https://example.com/part',
|
||||
)
|
||||
stock_attachment = Attachment.objects.create(
|
||||
model_type='stockitem',
|
||||
model_id=item.pk,
|
||||
comment='stock attachment',
|
||||
link='https://example.com/stock',
|
||||
)
|
||||
|
||||
# User has no roles at all - should see nothing, and be denied on direct retrieve
|
||||
list_url = reverse('api-attachment-list')
|
||||
response = self.get(list_url, expected_code=200)
|
||||
result_ids = {result['pk'] for result in response.data}
|
||||
self.assertNotIn(part_attachment.pk, result_ids)
|
||||
self.assertNotIn(stock_attachment.pk, result_ids)
|
||||
|
||||
self.get(
|
||||
reverse('api-attachment-detail', kwargs={'pk': part_attachment.pk}),
|
||||
expected_code=403,
|
||||
)
|
||||
self.get(
|
||||
reverse('api-attachment-detail', kwargs={'pk': stock_attachment.pk}),
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# Grant 'view' permission on 'part' only
|
||||
self.assignRole('part.view')
|
||||
|
||||
response = self.get(list_url, expected_code=200)
|
||||
result_ids = {result['pk'] for result in response.data}
|
||||
self.assertIn(part_attachment.pk, result_ids)
|
||||
self.assertNotIn(stock_attachment.pk, result_ids)
|
||||
|
||||
self.get(
|
||||
reverse('api-attachment-detail', kwargs={'pk': part_attachment.pk}),
|
||||
expected_code=200,
|
||||
)
|
||||
self.get(
|
||||
reverse('api-attachment-detail', kwargs={'pk': stock_attachment.pk}),
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# Granting 'stock' view permission too now exposes both
|
||||
self.assignRole('stock.view')
|
||||
|
||||
response = self.get(list_url, expected_code=200)
|
||||
result_ids = {result['pk'] for result in response.data}
|
||||
self.assertIn(part_attachment.pk, result_ids)
|
||||
self.assertIn(stock_attachment.pk, result_ids)
|
||||
|
||||
self.get(
|
||||
reverse('api-attachment-detail', kwargs={'pk': stock_attachment.pk}),
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
|
||||
class AttachmentThumbnailAPITests(InvenTreeAPITestCase):
|
||||
"""Tests for thumbnail generation when uploading attachments via the API."""
|
||||
|
||||
Reference in New Issue
Block a user