mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-04-27 18:39:27 +00:00
Merge branch 'main' into dev
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "paperless-ngx"
|
||||
version = "2.20.14"
|
||||
version = "2.20.15"
|
||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperless-ngx-ui",
|
||||
"version": "2.20.14",
|
||||
"version": "2.20.15",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"ng": "ng",
|
||||
|
||||
@@ -6,7 +6,7 @@ export const environment = {
|
||||
apiVersion: '10', // match src/paperless/settings.py
|
||||
appTitle: 'Paperless-ngx',
|
||||
tag: 'prod',
|
||||
version: '2.20.14',
|
||||
version: '2.20.15',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
||||
@@ -179,3 +179,28 @@ class TestPaperlessAdmin(DirectoriesMixin, TestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
superuser.refresh_from_db()
|
||||
self.assertEqual(superuser.first_name, "Updated")
|
||||
|
||||
def test_superuser_can_only_be_deleted_by_superuser(self):
|
||||
superuser = User.objects.create_superuser(username="superuser", password="test")
|
||||
user = User.objects.create(
|
||||
username="test",
|
||||
is_superuser=False,
|
||||
is_staff=True,
|
||||
)
|
||||
delete_user_perm = Permission.objects.get(codename="delete_user")
|
||||
user.user_permissions.add(delete_user_perm)
|
||||
|
||||
self.client.force_login(user)
|
||||
response = self.client.delete(f"/api/users/{superuser.pk}/")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(
|
||||
response.content.decode(),
|
||||
"Superusers can only be deleted by other superusers",
|
||||
)
|
||||
self.assertTrue(User.objects.filter(pk=superuser.pk).exists())
|
||||
|
||||
self.client.logout()
|
||||
self.client.force_login(superuser)
|
||||
response = self.client.delete(f"/api/users/{superuser.pk}/")
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(User.objects.filter(pk=superuser.pk).exists())
|
||||
|
||||
48
src/paperless/tests/test_api_auth.py
Normal file
48
src/paperless/tests/test_api_auth.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test import override_settings
|
||||
from django.urls import resolve
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class TestApiAuthViews(TestCase):
|
||||
def test_api_auth_login_uses_allauth_login_view(self):
|
||||
response = self.client.get(reverse("rest_framework:login"))
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTemplateUsed(response, "account/login.html")
|
||||
|
||||
def test_api_auth_login_uses_same_view_as_account_login(self):
|
||||
api_match = resolve("/api/auth/login/")
|
||||
account_match = resolve("/accounts/login/")
|
||||
|
||||
self.assertIs(api_match.func.view_class, account_match.func.view_class)
|
||||
|
||||
@override_settings(DISABLE_REGULAR_LOGIN=True)
|
||||
def test_api_auth_login_respects_disable_regular_login(self):
|
||||
username = f"testuser-{uuid.uuid4().hex}"
|
||||
User.objects.create_user(
|
||||
username=username,
|
||||
password="testpassword",
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("rest_framework:login"),
|
||||
data={
|
||||
"login": username,
|
||||
"password": "testpassword",
|
||||
"next": "/api/documents/",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTemplateUsed(response, "account/login.html")
|
||||
self.assertContains(response, "Regular login is disabled")
|
||||
self.assertNotIn("_auth_user_id", self.client.session)
|
||||
|
||||
def test_api_auth_logout_uses_named_route(self):
|
||||
self.assertEqual(reverse("rest_framework:login"), "/api/auth/login/")
|
||||
self.assertEqual(reverse("rest_framework:logout"), "/api/auth/logout/")
|
||||
@@ -98,7 +98,21 @@ urlpatterns = [
|
||||
re_path(
|
||||
"^auth/",
|
||||
include(
|
||||
("rest_framework.urls", "rest_framework"),
|
||||
(
|
||||
[
|
||||
path(
|
||||
"login/",
|
||||
allauth_account_views.login,
|
||||
name="login",
|
||||
),
|
||||
path(
|
||||
"logout/",
|
||||
allauth_account_views.logout,
|
||||
name="logout",
|
||||
),
|
||||
],
|
||||
"rest_framework",
|
||||
),
|
||||
namespace="rest_framework",
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[tuple[int, int, int]] = (2, 20, 14)
|
||||
__version__: Final[tuple[int, int, int]] = (2, 20, 15)
|
||||
# Version string like X.Y.Z
|
||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||
# Version string like X.Y
|
||||
|
||||
@@ -192,6 +192,16 @@ class UserViewSet(ModelViewSet[User]):
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
user_to_delete: User = self.get_object()
|
||||
|
||||
if not request.user.is_superuser and user_to_delete.is_superuser:
|
||||
return HttpResponseForbidden(
|
||||
"Superusers can only be deleted by other superusers",
|
||||
)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
request=None,
|
||||
responses={
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.serialisers import CorrespondentField
|
||||
from documents.serialisers import DocumentTypeField
|
||||
@@ -59,7 +60,18 @@ class MailAccountSerializer(OwnedObjectSerializer):
|
||||
|
||||
class AccountField(serializers.PrimaryKeyRelatedField[MailAccount]):
|
||||
def get_queryset(self):
|
||||
return MailAccount.objects.all().order_by("-id")
|
||||
user = getattr(self.context.get("request"), "user", None)
|
||||
if user is None:
|
||||
user = getattr(self.root, "user", None)
|
||||
|
||||
if user is None:
|
||||
return MailAccount.objects.none()
|
||||
|
||||
return get_objects_for_user_owner_aware(
|
||||
user,
|
||||
"change_mailaccount",
|
||||
MailAccount,
|
||||
).order_by("-id")
|
||||
|
||||
|
||||
class MailRuleSerializer(OwnedObjectSerializer):
|
||||
|
||||
@@ -493,7 +493,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(returned_rule1.name, "Updated Name 1")
|
||||
self.assertEqual(returned_rule1.action, MailRule.MailAction.DELETE)
|
||||
|
||||
def test_create_mail_rule_forbidden_for_unpermitted_account(self) -> None:
|
||||
def test_create_mail_rule_scopes_accounts(self) -> None:
|
||||
other_user = User.objects.create_user(username="mail-owner")
|
||||
foreign_account = MailAccountFactory(name="ForeignEmail", owner=other_user)
|
||||
|
||||
@@ -512,8 +512,26 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
|
||||
"attachment_type": MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
|
||||
},
|
||||
)
|
||||
missing_response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
data={
|
||||
"name": "Rule1",
|
||||
"account": foreign_account.pk + 1000,
|
||||
"folder": "INBOX",
|
||||
"filter_from": "from@example.com",
|
||||
"maximum_age": 30,
|
||||
"action": MailRule.MailAction.MARK_READ,
|
||||
"assign_title_from": MailRule.TitleSource.FROM_SUBJECT,
|
||||
"assign_correspondent_from": MailRule.CorrespondentSource.FROM_NOTHING,
|
||||
"order": 0,
|
||||
"attachment_type": MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(missing_response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data["account"][0].code, "does_not_exist")
|
||||
self.assertEqual(missing_response.data["account"][0].code, "does_not_exist")
|
||||
self.assertEqual(MailRule.objects.count(), 0)
|
||||
|
||||
def test_create_mail_rule_allowed_for_granted_account_change_permission(
|
||||
@@ -553,7 +571,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
|
||||
data={"account": foreign_account.pk},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
rule1.refresh_from_db()
|
||||
self.assertEqual(rule1.account, own_account)
|
||||
|
||||
|
||||
@@ -129,3 +129,5 @@ custom_fences = [
|
||||
[project.markdown_extensions.pymdownx.emoji]
|
||||
emoji_index = "zensical.extensions.emoji.twemoji"
|
||||
emoji_generator = "zensical.extensions.emoji.to_svg"
|
||||
|
||||
[project.markdown_extensions.zensical.extensions.glightbox]
|
||||
|
||||
Reference in New Issue
Block a user