mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-15 12:37:13 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c50c6f5517 |
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import ClassVar
|
||||
from typing import Literal
|
||||
from typing import TypeGuard
|
||||
from typing import TypeVar
|
||||
from typing import cast
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import Permission
|
||||
@@ -68,23 +62,28 @@ class PaperlessAdminPermissions(BasePermission):
|
||||
return request.user.is_active and request.user.is_staff
|
||||
|
||||
|
||||
def _is_active_and_authenticated(user: User | None) -> TypeGuard[User]:
|
||||
return (
|
||||
user is not None
|
||||
and getattr(user, "is_active", False)
|
||||
and getattr(user, "is_authenticated", False)
|
||||
)
|
||||
|
||||
|
||||
def has_global_statistics_permission(user: User | None) -> bool:
|
||||
return _is_active_and_authenticated(user) and (
|
||||
getattr(user, "is_superuser", False)
|
||||
or user.has_perm("paperless.view_global_statistics")
|
||||
if (
|
||||
user is None
|
||||
or not getattr(user, "is_active", False)
|
||||
or not getattr(user, "is_authenticated", False)
|
||||
):
|
||||
return False
|
||||
|
||||
return getattr(user, "is_superuser", False) or user.has_perm(
|
||||
"paperless.view_global_statistics",
|
||||
)
|
||||
|
||||
|
||||
def has_system_status_permission(user: User | None) -> bool:
|
||||
return _is_active_and_authenticated(user) and (
|
||||
if (
|
||||
user is None
|
||||
or not getattr(user, "is_active", False)
|
||||
or not getattr(user, "is_authenticated", False)
|
||||
):
|
||||
return False
|
||||
|
||||
return (
|
||||
getattr(user, "is_superuser", False)
|
||||
or getattr(user, "is_staff", False)
|
||||
or user.has_perm("paperless.view_system_monitoring")
|
||||
@@ -706,32 +705,7 @@ def has_perms_owner_aware(user, perms, obj):
|
||||
return obj.owner is None or obj.owner == user or checker.has_perm(perms, obj)
|
||||
|
||||
|
||||
# HEAD is deliberately absent: GlobalPermissionsByMethod checks it against GET.
|
||||
MappedMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
||||
|
||||
|
||||
class GlobalPermissionsByMethod(BasePermission):
|
||||
"""
|
||||
Requires the global (model-level) permissions listed in ``perms_map`` for
|
||||
the request's HTTP method. HEAD is checked against the ``"GET"`` entry,
|
||||
since DRF runs the GET handler for it. A method missing from
|
||||
``perms_map`` is denied.
|
||||
"""
|
||||
|
||||
perms_map: ClassVar[Mapping[MappedMethod, Sequence[str]]]
|
||||
|
||||
def has_permission(self, request: Any, view: Any) -> bool:
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
method = "GET" if request.method == "HEAD" else request.method
|
||||
# An unknown method is not a MappedMethod, but .get() returns None for
|
||||
# it, so it is still denied.
|
||||
perms = self.perms_map.get(cast("MappedMethod", method))
|
||||
return perms is not None and request.user.has_perms(perms)
|
||||
|
||||
|
||||
class ViewDocumentsPermissions(GlobalPermissionsByMethod):
|
||||
class ViewDocumentsPermissions(BasePermission):
|
||||
"""
|
||||
Permissions class that checks for model permissions for only viewing Documents.
|
||||
"""
|
||||
@@ -742,18 +716,31 @@ class ViewDocumentsPermissions(GlobalPermissionsByMethod):
|
||||
"POST": ["documents.view_document"],
|
||||
}
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or (not request.user.is_authenticated): # pragma: no cover
|
||||
return False
|
||||
|
||||
class TrashPermissions(GlobalPermissionsByMethod):
|
||||
return request.user.has_perms(self.perms_map.get(request.method, []))
|
||||
|
||||
|
||||
class TrashPermissions(BasePermission):
|
||||
"""Check the global document permission for each trash operation."""
|
||||
|
||||
perms_map = {
|
||||
"OPTIONS": ["documents.view_document"],
|
||||
"HEAD": ["documents.view_document"],
|
||||
"GET": ["documents.view_document"],
|
||||
"POST": ["documents.delete_document"],
|
||||
}
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated: # pragma: no cover
|
||||
return False
|
||||
|
||||
class PaperlessNotePermissions(GlobalPermissionsByMethod):
|
||||
return request.user.has_perms(self.perms_map.get(request.method, []))
|
||||
|
||||
|
||||
class PaperlessNotePermissions(BasePermission):
|
||||
"""
|
||||
Permissions class that checks for model permissions for Notes.
|
||||
"""
|
||||
@@ -773,13 +760,28 @@ class PaperlessNotePermissions(GlobalPermissionsByMethod):
|
||||
],
|
||||
}
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or (not request.user.is_authenticated): # pragma: no cover
|
||||
return False
|
||||
|
||||
class AcknowledgeTasksPermissions(GlobalPermissionsByMethod):
|
||||
perms = self.perms_map[request.method]
|
||||
|
||||
return request.user.has_perms(perms)
|
||||
|
||||
|
||||
class AcknowledgeTasksPermissions(BasePermission):
|
||||
"""
|
||||
Permissions class that checks for model permissions for acknowledging tasks.
|
||||
"""
|
||||
|
||||
perms_map = {
|
||||
"OPTIONS": ["documents.change_paperlesstask"],
|
||||
"POST": ["documents.change_paperlesstask"],
|
||||
}
|
||||
|
||||
def has_permission(self, request: Any, view: Any) -> bool:
|
||||
if not request.user or not request.user.is_authenticated: # pragma: no cover
|
||||
return False
|
||||
|
||||
perms = self.perms_map.get(request.method, [])
|
||||
|
||||
return request.user.has_perms(perms)
|
||||
@@ -3653,34 +3653,6 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_notes_head_and_unsupported_methods(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- A user allowed to view notes on their own document
|
||||
WHEN:
|
||||
- The notes endpoint is requested with HEAD, PUT and PATCH
|
||||
THEN:
|
||||
- HEAD is allowed like GET
|
||||
- PUT and PATCH are rejected with 403 rather than a server error
|
||||
"""
|
||||
user = UserFactory()
|
||||
grant_global(user, "view_note", "view_document")
|
||||
doc = DocumentFactory(owner=user)
|
||||
self.client.force_authenticate(user)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.head(f"/api/documents/{doc.pk}/notes/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.put(f"/api/documents/{doc.pk}/notes/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.patch(f"/api/documents/{doc.pk}/notes/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
def test_delete_note(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -1001,21 +1001,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 10)
|
||||
|
||||
def test_search_autocomplete_head_requires_view_document(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- A user without the global view document permission
|
||||
WHEN:
|
||||
- A HEAD request is made for autocomplete
|
||||
THEN:
|
||||
- It is rejected like the equivalent GET request
|
||||
"""
|
||||
self.client.force_authenticate(user=UserFactory())
|
||||
|
||||
response = self.client.head("/api/search/autocomplete/?term=test")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_search_autocomplete_respect_permissions(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user