diff --git a/docs/changelog.md b/docs/changelog.md index fc66aead3..fb4229e5a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,7 @@ # Changelog +## paperless-ngx 2.20.8 + ## paperless-ngx 2.20.7 ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index 673fd5c0b..7edde8dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.7" +version = "2.20.8" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" diff --git a/src-ui/package.json b/src-ui/package.json index e3528a45a..59d29ca74 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.7", + "version": "2.20.8", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 819bbed0c..0ad536f2b 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.7', + version: '2.20.8', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src/paperless/version.py b/src/paperless/version.py index 7515698ad..a2f677230 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 7) +__version__: Final[tuple[int, int, int]] = (2, 20, 8) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/src/paperless_mail/tests/test_api.py b/src/paperless_mail/tests/test_api.py index dba8c840c..b5de98b50 100644 --- a/src/paperless_mail/tests/test_api.py +++ b/src/paperless_mail/tests/test_api.py @@ -272,6 +272,24 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["success"], True) + def test_mail_account_test_existing_nonexistent_id_forbidden(self): + response = self.client.post( + f"{self.ENDPOINT}test/", + json.dumps( + { + "id": 999999, + "imap_server": "server.example.com", + "imap_port": 443, + "imap_security": MailAccount.ImapSecurity.SSL, + "username": "admin", + "password": "******", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content.decode(), "Insufficient permissions") + def test_get_mail_accounts_owner_aware(self) -> None: """ GIVEN: diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 4dffd677e..fe45adbfd 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -8,6 +8,7 @@ from datetime import timedelta from unittest import mock import pytest +from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.core.management import call_command from django.db import DatabaseError @@ -1734,6 +1735,10 @@ class TestMailAccountTestView(APITestCase): username="testuser", password="testpassword", ) + self.user.user_permissions.add( + *Permission.objects.filter(codename__in=["add_mailaccount"]), + ) + self.user.save() self.client.force_authenticate(user=self.user) self.url = "/api/mail_accounts/test/" @@ -1850,6 +1855,54 @@ class TestMailAccountTestView(APITestCase): expected_str = "Unable to refresh oauth token" self.assertIn(expected_str, error_str) + def test_mail_account_test_view_existing_forbidden_for_other_owner(self): + other_user = User.objects.create_user( + username="otheruser", + password="testpassword", + ) + existing_account = MailAccount.objects.create( + name="Owned account", + imap_server="imap.example.com", + imap_port=993, + imap_security=MailAccount.ImapSecurity.SSL, + username="admin", + password="secret", + owner=other_user, + ) + data = { + "id": existing_account.id, + "imap_server": "imap.example.com", + "imap_port": 993, + "imap_security": MailAccount.ImapSecurity.SSL, + "username": "admin", + "password": "****", + "is_token": False, + } + + response = self.client.post(self.url, data, format="json") + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content.decode(), "Insufficient permissions") + + def test_mail_account_test_view_requires_add_permission_without_account_id(self): + self.user.user_permissions.remove( + *Permission.objects.filter(codename__in=["add_mailaccount"]), + ) + self.user.save() + data = { + "imap_server": "imap.example.com", + "imap_port": 993, + "imap_security": MailAccount.ImapSecurity.SSL, + "username": "admin", + "password": "secret", + "is_token": False, + } + + response = self.client.post(self.url, data, format="json") + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content.decode(), "Insufficient permissions") + class TestMailAccountProcess(APITestCase): def setUp(self) -> None: diff --git a/src/paperless_mail/views.py b/src/paperless_mail/views.py index b54bcb5f7..b8ac2c485 100644 --- a/src/paperless_mail/views.py +++ b/src/paperless_mail/views.py @@ -86,13 +86,34 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin): request.data["name"] = datetime.datetime.now().isoformat() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + existing_account = None + account_id = request.data.get("id") - # account exists, use the password from there instead of *** and refresh_token / expiration + # testing a new connection requires add permission + if account_id is None and not request.user.has_perms( + ["paperless_mail.add_mailaccount"], + ): + return HttpResponseForbidden("Insufficient permissions") + + # testing an existing account requires change permission on that account + if account_id is not None: + try: + existing_account = MailAccount.objects.get(pk=account_id) + except (TypeError, ValueError, MailAccount.DoesNotExist): + return HttpResponseForbidden("Insufficient permissions") + + if not has_perms_owner_aware( + request.user, + "change_mailaccount", + existing_account, + ): + return HttpResponseForbidden("Insufficient permissions") + + # account exists, use the password from there instead of *** if ( len(serializer.validated_data.get("password").replace("*", "")) == 0 - and request.data["id"] is not None + and existing_account is not None ): - existing_account = MailAccount.objects.get(pk=request.data["id"]) serializer.validated_data["password"] = existing_account.password serializer.validated_data["account_type"] = existing_account.account_type serializer.validated_data["refresh_token"] = existing_account.refresh_token @@ -106,7 +127,8 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin): ) as M: try: if ( - account.is_token + existing_account is not None + and account.is_token and account.expiration is not None and account.expiration < timezone.now() ): @@ -248,6 +270,7 @@ class OauthCallbackView(GenericAPIView): imap_server=imap_server, refresh_token=refresh_token, expiration=timezone.now() + timedelta(seconds=expires_in), + owner=request.user, defaults=defaults, ) return HttpResponseRedirect( diff --git a/uv.lock b/uv.lock index 07f521e19..2b1ee98b1 100644 --- a/uv.lock +++ b/uv.lock @@ -3019,7 +3019,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.7" +version = "2.20.8" source = { virtual = "." } dependencies = [ { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },