mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-31 20:15:44 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a305aad7e |
No files matched your search
+21
-28
@@ -15,7 +15,6 @@ import dateparser
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Case
|
||||
from django.db.models import CharField
|
||||
from django.db.models import Count
|
||||
from django.db.models import Exists
|
||||
from django.db.models import IntegerField
|
||||
@@ -208,36 +207,30 @@ class EffectiveContentFilter(Filter):
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
class SharedByUser(Filter):
|
||||
def filter(self, qs, value):
|
||||
if value is None:
|
||||
return qs
|
||||
|
||||
ctype = ContentType.objects.get_for_model(self.model)
|
||||
UserObjectPermission = get_user_obj_perms_model()
|
||||
GroupObjectPermission = get_group_obj_perms_model()
|
||||
# see https://github.com/paperless-ngx/paperless-ngx/issues/5392, we limit subqueries
|
||||
# to 1 because Postgres doesn't like returning > 1 row, but all we care about is > 0
|
||||
return (
|
||||
qs.filter(
|
||||
owner_id=value,
|
||||
)
|
||||
.annotate(
|
||||
num_shared_users=Count(
|
||||
UserObjectPermission.objects.filter(
|
||||
content_type=ctype,
|
||||
object_pk=Cast(OuterRef("pk"), CharField()),
|
||||
).values("user_id")[:1],
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
num_shared_groups=Count(
|
||||
GroupObjectPermission.objects.filter(
|
||||
content_type=ctype,
|
||||
object_pk=Cast(OuterRef("pk"), CharField()),
|
||||
).values("group_id")[:1],
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
Q(num_shared_users__gt=0) | Q(num_shared_groups__gt=0),
|
||||
)
|
||||
if value is not None
|
||||
else qs
|
||||
# Guardian stores object_pk as a string. Each grant table is read once,
|
||||
# as an uncorrelated set of integer keys, because probing it per
|
||||
# document by string key has no usable index (none leads with
|
||||
# object_pk) and MariaDB re-scans the table for every owned document.
|
||||
# DISTINCT keeps Postgres hashing each set once: without it a large
|
||||
# group grant table exceeds work_mem and is re-scanned for every row.
|
||||
shared_with_users = (
|
||||
UserObjectPermission.objects.filter(content_type=ctype)
|
||||
.values(shared_pk=Cast("object_pk", IntegerField()))
|
||||
.distinct()
|
||||
)
|
||||
shared_with_groups = (
|
||||
GroupObjectPermission.objects.filter(content_type=ctype)
|
||||
.values(shared_pk=Cast("object_pk", IntegerField()))
|
||||
.distinct()
|
||||
)
|
||||
return qs.filter(owner_id=value).filter(
|
||||
Q(pk__in=shared_with_users) | Q(pk__in=shared_with_groups),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -291,19 +291,12 @@ def get_permission_grantees(
|
||||
"permission__codename__in": list(action_by_codename),
|
||||
"object_pk__in": [str(pk) for pk in pks],
|
||||
}
|
||||
for perm_model, identity_model, identity_field, key in (
|
||||
(UserObjectPermission, User, "user_id", "users"),
|
||||
(GroupObjectPermission, Group, "group_id", "groups"),
|
||||
for perm_model, identity_field, key in (
|
||||
(UserObjectPermission, "user_id", "users"),
|
||||
(GroupObjectPermission, "group_id", "groups"),
|
||||
):
|
||||
# Every grant belongs to an existing user or group, so restricting to
|
||||
# them changes no result. It lets MariaDB use guardian's
|
||||
# (identity, content_type, object_pk) index; the content type alone
|
||||
# matches every grant of the model and is scanned in full.
|
||||
rows = (
|
||||
perm_model.objects.filter(
|
||||
**{f"{identity_field}__in": identity_model.objects.values("pk")},
|
||||
**grant_filter,
|
||||
)
|
||||
perm_model.objects.filter(**grant_filter)
|
||||
.values_list("object_pk", "permission__codename", identity_field)
|
||||
.order_by(identity_field)
|
||||
)
|
||||
|
||||
@@ -397,14 +397,10 @@ class OwnedObjectSerializer(
|
||||
object_pks = list(obj.pk for obj in objects)
|
||||
pk_type = type(first_obj.pk)
|
||||
|
||||
def get_pks_for_permission_type(model, identity_field, identity_model):
|
||||
# Restricting to existing users/groups changes no result but lets
|
||||
# MariaDB use guardian's (identity, content_type, object_pk) index
|
||||
# instead of scanning every grant of the model.
|
||||
def get_pks_for_permission_type(model):
|
||||
return map(
|
||||
pk_type, # coerce the pk to be the same type of the provided objects
|
||||
model.objects.filter(
|
||||
**{f"{identity_field}__in": identity_model.objects.values("pk")},
|
||||
content_type=ctype,
|
||||
object_pk__in=object_pks,
|
||||
)
|
||||
@@ -414,16 +410,8 @@ class OwnedObjectSerializer(
|
||||
|
||||
UserObjectPermission = get_user_obj_perms_model()
|
||||
GroupObjectPermission = get_group_obj_perms_model()
|
||||
user_permission_pks = get_pks_for_permission_type(
|
||||
UserObjectPermission,
|
||||
"user_id",
|
||||
User,
|
||||
)
|
||||
group_permission_pks = get_pks_for_permission_type(
|
||||
GroupObjectPermission,
|
||||
"group_id",
|
||||
Group,
|
||||
)
|
||||
user_permission_pks = get_pks_for_permission_type(UserObjectPermission)
|
||||
group_permission_pks = get_pks_for_permission_type(GroupObjectPermission)
|
||||
|
||||
return set(user_permission_pks) | set(group_permission_pks)
|
||||
|
||||
|
||||
@@ -1480,6 +1480,45 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
||||
[u1_doc1.id],
|
||||
)
|
||||
|
||||
def test_shared_by_filter_matches_user_and_group_grants_once(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Documents owned by a user: one shared with a user, one shared with
|
||||
a group, one shared with several users and a group, one unshared
|
||||
- A shared document owned by someone else
|
||||
WHEN:
|
||||
- Documents are filtered by shared_by__id for the owner
|
||||
THEN:
|
||||
- Only the owner's shared documents are returned, each exactly once
|
||||
"""
|
||||
owner = UserFactory(username="owner")
|
||||
other = UserFactory(username="other")
|
||||
reader1 = UserFactory(username="reader1")
|
||||
reader2 = UserFactory(username="reader2")
|
||||
group = Group.objects.create(name="readers")
|
||||
grant_all_global(owner)
|
||||
|
||||
shared_with_user = DocumentFactory.create(owner=owner)
|
||||
shared_with_group = DocumentFactory.create(owner=owner)
|
||||
shared_widely = DocumentFactory.create(owner=owner)
|
||||
DocumentFactory.create(owner=owner)
|
||||
others_shared = DocumentFactory.create(owner=other)
|
||||
grant_object(reader1, shared_with_user, "view_document")
|
||||
grant_object(group, shared_with_group, "view_document")
|
||||
for target in (reader1, reader2, group):
|
||||
grant_object(target, shared_widely, "view_document")
|
||||
grant_object(reader1, others_shared, "view_document")
|
||||
|
||||
self.client.force_authenticate(owner)
|
||||
response = self.client.get(f"/api/documents/?shared_by__id={owner.id}")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["count"], 3)
|
||||
self.assertCountEqual(
|
||||
[result["id"] for result in response.data["results"]],
|
||||
[shared_with_user.id, shared_with_group.id, shared_widely.id],
|
||||
)
|
||||
|
||||
def test_document_owned_and_group_shared_not_duplicated_when_filtering_by_tags(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
Reference in new issue
Block a user