Compare commits

..
Author SHA1 Message Date
dependabot[bot] c50c6f5517 Chore(deps): Bump filelock from 3.32.6 to 4.0.12 (#14336)
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.32.6 to 4.0.12.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.32.6...4.0.12)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 16:11:12 -07:00
7 changed files with 85 additions and 108 deletions

No files matched your search

+1 -1
View File
@@ -43,7 +43,7 @@ dependencies = [
"drf-spectacular~=0.30",
"drf-spectacular-sidecar>=2026.7.1,<2026.10",
"drf-writable-nested~=0.7.1",
"filelock~=3.32.0",
"filelock>=3.32,<4.1",
"flower>=2.0.1,<2.3",
"gotenberg-client[httpx]~=1.0",
"httpx-oauth~=0.17",
+36
View File
@@ -21,6 +21,7 @@ from guardian.core import ObjectPermissionChecker
from guardian.models import GroupObjectPermission
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_objects_for_user
from guardian.shortcuts import get_users_with_perms
from guardian.shortcuts import remove_perm
from rest_framework.permissions import BasePermission
@@ -648,6 +649,41 @@ def annotate_document_count_for_related_queryset(
)
def get_objects_for_user_owner_aware(
user: User | None,
perms: str | list[str],
Model: Any,
*,
include_deleted: bool = False,
) -> QuerySet[Any]:
"""
Returns objects the user owns, are unowned, or has explicit perms.
When include_deleted is True, soft-deleted items are also included.
Legacy slow path (guardian-backed, O(n) style permission resolution).
Most queryset-filtering call sites have migrated onto
``PermittedObjectsFilter``/``permitted_object_ids()``, but this function
is kept because production callers still remain. Several callers remain
across ``documents/``, ``paperless_mail/``, and ``paperless_ai/`` --
grep for this function name before removing it.
"""
manager = (
Model.global_objects
if include_deleted and hasattr(Model, "global_objects")
else Model.objects
)
objects_owned = manager.filter(owner=user)
objects_unowned = manager.filter(owner__isnull=True)
objects_with_perms = get_objects_for_user(
user=user,
perms=perms,
klass=manager.all(),
accept_global_perms=False,
)
return objects_owned | objects_unowned | objects_with_perms
def has_perms_owner_aware(user, perms, obj):
"""
Legacy slow path (guardian-backed) single-object permission check.
+10 -6
View File
@@ -52,7 +52,7 @@ from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowRun
from documents.models import WorkflowTrigger
from documents.permissions import restrict_queryset_to_visible
from documents.permissions import get_objects_for_user_owner_aware
from documents.plugins.helpers import DocumentsStatusManager
from documents.templating.utils import convert_format_str_to_template_format
from documents.utils import compute_checksum
@@ -81,11 +81,15 @@ DRF_DATETIME_FIELD = serializers.DateTimeField()
def add_inbox_tags(sender, document: Document, logging_group=None, **kwargs) -> None:
inbox_tags = restrict_queryset_to_visible(
Tag.objects.filter(is_inbox_tag=True),
document.owner,
"view_tag",
)
if document.owner is not None:
tags = get_objects_for_user_owner_aware(
document.owner,
"documents.view_tag",
Tag,
)
else:
tags = Tag.objects.all()
inbox_tags = tags.filter(is_inbox_tag=True)
document.add_nested_tags(inbox_tags)
-61
View File
@@ -34,10 +34,8 @@ from documents.tests.utils import GetConsumerMixin
from paperless_mail.models import MailRule
from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.fakes.progress import FakeProgressManager
from paperless_testing.permissions import grant_object
class _BaseNewStyleParser:
@@ -516,65 +514,6 @@ class TestConsumer(
self.assertTrue(group_checker.has_perm("view_document", document))
self._assert_first_last_send_progress()
def testInboxTagsScopedToOwner(self) -> None:
"""
GIVEN:
- Inbox tags that are owned by the document owner, unowned, owned by
another user but shared with the document owner, and owned by
another user and not shared
- A non-inbox tag owned by the document owner
WHEN:
- A document is consumed with that owner
THEN:
- The document gets the owned, unowned and shared inbox tags only
"""
owner = UserFactory()
other = UserFactory()
owned = TagFactory(is_inbox_tag=True, owner=owner)
unowned = TagFactory(is_inbox_tag=True, owner=None)
shared = TagFactory(is_inbox_tag=True, owner=other)
grant_object(owner, shared, "view_tag")
TagFactory(is_inbox_tag=True, owner=other)
TagFactory(is_inbox_tag=False, owner=owner)
with self.get_consumer(
self.get_test_file(),
DocumentMetadataOverrides(owner_id=owner.pk),
) as consumer:
consumer.run()
document = Document.objects.first()
assert document is not None
self.assertCountEqual(document.tags.all(), [owned, unowned, shared])
self._assert_first_last_send_progress()
def testInboxTagsUnownedDocument(self) -> None:
"""
GIVEN:
- Inbox tags owned by two different users, an unowned inbox tag and
a non-inbox tag
WHEN:
- A document is consumed with no owner
THEN:
- The document gets every inbox tag
"""
inbox_tags = [
TagFactory(is_inbox_tag=True, owner=UserFactory()),
TagFactory(is_inbox_tag=True, owner=UserFactory()),
TagFactory(is_inbox_tag=True, owner=None),
]
TagFactory(is_inbox_tag=False)
with self.get_consumer(self.get_test_file()) as consumer:
consumer.run()
document = Document.objects.first()
assert document is not None
self.assertCountEqual(document.tags.all(), inbox_tags)
self._assert_first_last_send_progress()
def testNotAFile(self) -> None:
with self.assertRaisesMessage(ConsumerError, "File not found"):
with self.get_consumer(Path("non-existing-file")) as consumer:
+31 -33
View File
@@ -175,12 +175,12 @@ from documents.permissions import ViewDocumentsPermissions
from documents.permissions import annotate_document_count_by_ids
from documents.permissions import annotate_document_count_for_related_queryset
from documents.permissions import get_document_count_filter_for_user
from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import has_global_statistics_permission
from documents.permissions import has_perms_owner_aware
from documents.permissions import has_system_status_permission
from documents.permissions import permitted_document_ids
from documents.permissions import permitted_object_ids
from documents.permissions import restrict_queryset_to_visible
from documents.permissions import set_permissions_for_objects
from documents.permissions import user_is_unrestricted
from documents.plugins.date_parsing import get_date_parser
@@ -3836,51 +3836,49 @@ class GlobalSearchView(PassUserMixin):
if doc_id in docs_by_id
][:OBJECT_LIMIT]
saved_views = (
restrict_queryset_to_visible(
SavedView.objects.filter(name__icontains=query),
get_objects_for_user_owner_aware(
request.user,
"view_savedview",
)
SavedView,
).filter(name__icontains=query)
if request.user.has_perm("documents.view_savedview")
else []
)
saved_views = saved_views[:OBJECT_LIMIT]
tags = (
restrict_queryset_to_visible(
Tag.objects.filter(name__icontains=query),
request.user,
"view_tag",
get_objects_for_user_owner_aware(request.user, "view_tag", Tag).filter(
name__icontains=query,
)
if request.user.has_perm("documents.view_tag")
else []
)
tags = tags[:OBJECT_LIMIT]
correspondents = (
restrict_queryset_to_visible(
Correspondent.objects.filter(name__icontains=query),
get_objects_for_user_owner_aware(
request.user,
"view_correspondent",
)
Correspondent,
).filter(name__icontains=query)
if request.user.has_perm("documents.view_correspondent")
else []
)
correspondents = correspondents[:OBJECT_LIMIT]
document_types = (
restrict_queryset_to_visible(
DocumentType.objects.filter(name__icontains=query),
get_objects_for_user_owner_aware(
request.user,
"view_documenttype",
)
DocumentType,
).filter(name__icontains=query)
if request.user.has_perm("documents.view_documenttype")
else []
)
document_types = document_types[:OBJECT_LIMIT]
storage_paths = (
restrict_queryset_to_visible(
StoragePath.objects.filter(name__icontains=query),
get_objects_for_user_owner_aware(
request.user,
"view_storagepath",
)
StoragePath,
).filter(name__icontains=query)
if request.user.has_perm("documents.view_storagepath")
else []
)
@@ -3898,21 +3896,21 @@ class GlobalSearchView(PassUserMixin):
)
groups = groups[:OBJECT_LIMIT]
mail_rules = (
restrict_queryset_to_visible(
MailRule.objects.filter(name__icontains=query),
get_objects_for_user_owner_aware(
request.user,
"view_mailrule",
)
MailRule,
).filter(name__icontains=query)
if request.user.has_perm("paperless_mail.view_mailrule")
else []
)
mail_rules = mail_rules[:OBJECT_LIMIT]
mail_accounts = (
restrict_queryset_to_visible(
MailAccount.objects.filter(name__icontains=query),
get_objects_for_user_owner_aware(
request.user,
"view_mailaccount",
)
MailAccount,
).filter(name__icontains=query)
if request.user.has_perm("paperless_mail.view_mailaccount")
else []
)
@@ -4028,33 +4026,33 @@ class StatisticsView(GenericAPIView[Any]):
tags = (
Tag.objects.all()
if can_view_global_stats
else restrict_queryset_to_visible(Tag.objects.all(), user, "view_tag")
else get_objects_for_user_owner_aware(user, "documents.view_tag", Tag)
).only("id", "is_inbox_tag")
correspondent_count = (
Correspondent.objects.count()
if can_view_global_stats
else restrict_queryset_to_visible(
Correspondent.objects.all(),
else get_objects_for_user_owner_aware(
user,
"view_correspondent",
"documents.view_correspondent",
Correspondent,
).count()
)
document_type_count = (
DocumentType.objects.count()
if can_view_global_stats
else restrict_queryset_to_visible(
DocumentType.objects.all(),
else get_objects_for_user_owner_aware(
user,
"view_documenttype",
"documents.view_documenttype",
DocumentType,
).count()
)
storage_path_count = (
StoragePath.objects.count()
if can_view_global_stats
else restrict_queryset_to_visible(
StoragePath.objects.all(),
else get_objects_for_user_owner_aware(
user,
"view_storagepath",
"documents.view_storagepath",
StoragePath,
).count()
)
+3 -3
View File
@@ -2,8 +2,8 @@ from django.utils.translation import gettext as _
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import has_perms_owner_aware
from documents.permissions import restrict_queryset_to_visible
from documents.serialisers import CorrespondentField
from documents.serialisers import DocumentTypeField
from documents.serialisers import OwnedObjectSerializer
@@ -68,10 +68,10 @@ class AccountField(serializers.PrimaryKeyRelatedField[MailAccount]):
if user is None:
return MailAccount.objects.none()
return restrict_queryset_to_visible(
MailAccount.objects.all(),
return get_objects_for_user_owner_aware(
user,
"change_mailaccount",
MailAccount,
).order_by("-id")
Generated
+4 -4
View File
@@ -1272,11 +1272,11 @@ wheels = [
[[package]]
name = "filelock"
version = "3.32.6"
version = "4.0.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/46/126b1831dca12060d4a8296bf9c4fe5c93c4f22197fa239cb0cc82042bba/filelock-3.32.6.tar.gz", hash = "sha256:a3f55a18af3652a94d8f47d6055df434f254ca1d02ef2524850c6d249ca2512c", size = 225172, upload-time = "2026-09-08T22:57:11.528Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/e4/34efcb869715cf299e47d1ac7b2624d2bcb6f2d3dffc2f0abe8417f65ab2/filelock-4.0.12.tar.gz", hash = "sha256:cf42711a7ac791818b299fab0332a088c65aeeefa36290de98db92c434303b0c", size = 253880, upload-time = "2026-10-05T13:17:43.677Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/06/4f138f618dbea66803291274f228f01daf29f306fe8b96bc30dab765df75/filelock-3.32.6-py3-none-any.whl", hash = "sha256:3f16ecd0117feae0dfc147e8c62eb5daeccd8bd800378c3ddf416de9b4feb6b1", size = 100189, upload-time = "2026-09-08T22:57:10.182Z" },
{ url = "https://files.pythonhosted.org/packages/da/a1/b047e1c486a2c66283633b7dbb73872ca16ca1ce6a426f70d4d037fc8e1c/filelock-4.0.12-py3-none-any.whl", hash = "sha256:5f17ee83ecee8a6f3e389c75822fb70a1c2f0506b99438a6dffa1d56793588c8", size = 111732, upload-time = "2026-10-05T13:17:42.361Z" },
]
[[package]]
@@ -3184,7 +3184,7 @@ requires-dist = [
{ name = "drf-spectacular", specifier = "~=0.30" },
{ name = "drf-spectacular-sidecar", specifier = ">=2026.7.1,<2026.10" },
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
{ name = "filelock", specifier = "~=3.32.0" },
{ name = "filelock", specifier = ">=3.32,<4.1" },
{ name = "flower", specifier = ">=2.0.1,<2.3" },
{ name = "gotenberg-client", extras = ["httpx"], specifier = "~=1.0" },
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = ">=2.7,<2.9" },