mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-04 22:09:24 +00:00
Fix: check descendant tag permissions on delete (#14687)
This commit is contained in:
1 parent
22746aa202
commit
fa83a4c5dc
2 files changed
+82
No files matched your search
@@ -12,11 +12,73 @@ from documents.models import WorkflowTrigger
|
||||
from documents.serialisers import TagSerializer
|
||||
from documents.signals.handlers import run_workflows
|
||||
from paperless_testing.dirs import DirectoriesMixin
|
||||
from paperless_testing.factories import DocumentFactory
|
||||
from paperless_testing.factories import TagFactory
|
||||
from paperless_testing.factories import UserFactory
|
||||
from paperless_testing.permissions import grant_global
|
||||
from paperless_testing.permissions import grant_object
|
||||
|
||||
|
||||
class TestTagHierarchyPermissions(APITestCase):
|
||||
def test_delete_checks_descendant_permissions(self) -> None:
|
||||
for mode in ("detail", "bulk", "all"):
|
||||
for ownership in ("owned", "unowned", "private", "shared", "superuser"):
|
||||
with self.subTest(mode=mode, ownership=ownership):
|
||||
requester = UserFactory(superuser=ownership == "superuser")
|
||||
grant_global(requester, "view_tag", "delete_tag")
|
||||
self.client.force_authenticate(user=requester)
|
||||
if ownership == "owned":
|
||||
descendant_owner = requester
|
||||
elif ownership == "unowned":
|
||||
descendant_owner = None
|
||||
else:
|
||||
descendant_owner = UserFactory()
|
||||
parent = TagFactory(owner=requester)
|
||||
child = TagFactory(tn_parent=parent)
|
||||
grandchild = TagFactory(
|
||||
owner=descendant_owner,
|
||||
tn_parent=child,
|
||||
)
|
||||
if ownership == "shared":
|
||||
grant_object(requester, grandchild, "delete_tag")
|
||||
document = DocumentFactory()
|
||||
document.tags.add(grandchild)
|
||||
ids = [parent.pk, child.pk, grandchild.pk]
|
||||
|
||||
if mode == "detail":
|
||||
response = self.client.delete(f"/api/tags/{parent.pk}/")
|
||||
else:
|
||||
selection = (
|
||||
{"all": True, "filters": {"id__in": str(parent.pk)}}
|
||||
if mode == "all"
|
||||
else {"objects": [parent.pk]}
|
||||
)
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_objects/",
|
||||
{"object_type": "tags", "operation": "delete", **selection},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Detail deletion accepts explicit delete grants; bulk
|
||||
# deletion requires ownership (or an unowned object).
|
||||
allowed = ownership in ("owned", "unowned", "superuser") or (
|
||||
ownership == "shared" and mode == "detail"
|
||||
)
|
||||
expected_status = status.HTTP_403_FORBIDDEN
|
||||
if allowed:
|
||||
expected_status = (
|
||||
status.HTTP_204_NO_CONTENT
|
||||
if mode == "detail"
|
||||
else status.HTTP_200_OK
|
||||
)
|
||||
assert response.status_code == expected_status
|
||||
assert Tag.objects.filter(pk__in=ids).count() == (
|
||||
0 if allowed else 3
|
||||
)
|
||||
assert document.tags.filter(pk=grandchild.pk).exists() == (
|
||||
not allowed
|
||||
)
|
||||
|
||||
def test_children_only_include_visible_tags(self) -> None:
|
||||
owner = UserFactory(username="owner")
|
||||
requester = UserFactory(username="requester")
|
||||
|
||||
@@ -576,6 +576,15 @@ class CorrespondentViewSet(
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def _with_tag_descendants(tags: QuerySet[Tag]) -> QuerySet[Tag]:
|
||||
"""Include every tag that deleting the selected tags will affect."""
|
||||
tag_ids = set()
|
||||
for tag in tags:
|
||||
tag_ids.add(tag.pk)
|
||||
tag_ids.update(tag.get_descendants_pks())
|
||||
return Tag.objects.filter(pk__in=tag_ids)
|
||||
|
||||
|
||||
@extend_schema_view(**generate_object_with_permissions_schema(TagSerializer))
|
||||
class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Tag]):
|
||||
model = Tag
|
||||
@@ -597,6 +606,14 @@ class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Tag]):
|
||||
filterset_class = TagFilterSet
|
||||
ordering_fields = ("color", "name", "matching_algorithm", "match", "document_count")
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
tags = _with_tag_descendants(Tag.objects.filter(pk=instance.pk))
|
||||
if tags.exclude(
|
||||
pk__in=permitted_object_ids(self.request.user, Tag, "delete_tag"),
|
||||
).exists():
|
||||
raise PermissionDenied(_("Insufficient permissions."))
|
||||
super().perform_destroy(instance)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["document_count_filter"] = self.get_document_count_filter()
|
||||
@@ -5075,6 +5092,9 @@ class BulkEditObjectsView(PassUserMixin):
|
||||
pk__in=object_ids,
|
||||
)
|
||||
|
||||
if object_type == "tags" and operation == "delete":
|
||||
objs = _with_tag_descendants(objs)
|
||||
|
||||
if not user.is_superuser:
|
||||
perm = f"documents.{perm_codename}"
|
||||
# Limited to the owner (or unowned), same as documents, see BulkEditView
|
||||
|
||||
Reference in new issue
Block a user