Compare commits

..

5 Commits

Author SHA1 Message Date
github-actions[bot]
1e00ad5f30 Documentation: Add v2.20.11 changelog (#12356)
* Changelog v2.20.11 - GHA

* Update changelog for version 2.20.11

Added security advisory and fixed dropdown list issues.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-03-15 18:47:18 -07:00
shamoon
5f26c01c6f Bump version to 2.20.11 2026-03-15 17:16:11 -07:00
shamoon
92e133eeb0 Merge branch 'release/v2.20.x' 2026-03-15 17:15:20 -07:00
shamoon
06b2d5102c Fix GHSA-59xh-5vwx-4c4q 2026-03-15 17:13:08 -07:00
shamoon
1e8622494d Documentation: remove broken link 2026-03-07 23:29:42 -08:00
11 changed files with 151 additions and 16 deletions

View File

@@ -1,5 +1,29 @@
# Changelog
## paperless-ngx 2.20.11
### Security
- Resolve [GHSA-59xh-5vwx-4c4q](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-59xh-5vwx-4c4q)
### Bug Fixes
- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328))
- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326))
- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302))
- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
### All App Changes
<details>
<summary>4 changes</summary>
- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328))
- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326))
- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302))
- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
</details>
## paperless-ngx 2.20.10
### Bug Fixes

View File

@@ -505,9 +505,8 @@ installation. Keep these points in mind:
- Read the [changelog](changelog.md) and
take note of breaking changes.
- Decide whether to stay on SQLite or migrate to PostgreSQL.
See [documentation](#sqlite_to_psql) for details on moving data
from SQLite to PostgreSQL. Both work fine with
Paperless. However, if you already have a database server running
Both work fine with Paperless-ngx.
However, if you already have a database server running
for other services, you might as well use it for Paperless as well.
- The task scheduler of Paperless, which is used to execute periodic
tasks such as email checking and maintenance, requires a

View File

@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "2.20.10"
version = "2.20.11"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.10"

View File

@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "2.20.10",
"version": "2.20.11",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",

View File

@@ -6,7 +6,7 @@ export const environment = {
apiVersion: '9', // match src/paperless/settings.py
appTitle: 'Paperless-ngx',
tag: 'prod',
version: '2.20.10',
version: '2.20.11',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@@ -888,6 +888,19 @@ class TestApiUser(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.post(
f"{self.ENDPOINT}",
json.dumps(
{
"username": "user4",
"is_superuser": "true",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.force_authenticate(user2)
response = self.client.patch(
@@ -920,6 +933,65 @@ class TestApiUser(DirectoriesMixin, APITestCase):
returned_user1 = User.objects.get(pk=user1.pk)
self.assertEqual(returned_user1.is_superuser, False)
def test_only_superusers_can_create_or_alter_staff_status(self):
"""
GIVEN:
- Existing user account
WHEN:
- API request is made to add a user account with staff status
- API request is made to change staff status
THEN:
- Only superusers can change staff status
"""
user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.all())
user2 = User.objects.create_superuser(username="user2")
self.client.force_authenticate(user1)
response = self.client.patch(
f"{self.ENDPOINT}{user1.pk}/",
json.dumps(
{
"is_staff": "true",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.post(
f"{self.ENDPOINT}",
json.dumps(
{
"username": "user3",
"is_staff": 1,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.force_authenticate(user2)
response = self.client.patch(
f"{self.ENDPOINT}{user1.pk}/",
json.dumps(
{
"is_staff": True,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
returned_user1 = User.objects.get(pk=user1.pk)
self.assertEqual(returned_user1.is_staff, True)
class TestApiGroup(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/groups/"

View File

@@ -1,6 +1,6 @@
from typing import Final
__version__: Final[tuple[int, int, int]] = (2, 20, 10)
__version__: Final[tuple[int, int, int]] = (2, 20, 11)
# Version string like X.Y.Z
__full_version_str__: Final[str] = ".".join(map(str, __version__))
# Version string like X.Y

View File

@@ -25,6 +25,8 @@ from drf_spectacular.utils import extend_schema_view
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField
from rest_framework.filters import OrderingFilter
from rest_framework.generics import GenericAPIView
from rest_framework.pagination import PageNumberPagination
@@ -103,6 +105,7 @@ class FaviconView(View):
class UserViewSet(ModelViewSet):
_BOOL_NOT_PROVIDED = object()
model = User
queryset = User.objects.exclude(
@@ -116,27 +119,65 @@ class UserViewSet(ModelViewSet):
filterset_class = UserFilterSet
ordering_fields = ("username",)
@staticmethod
def _parse_requested_bool(data, key: str):
if key not in data:
return UserViewSet._BOOL_NOT_PROVIDED
try:
return BooleanField().to_internal_value(data.get(key))
except ValidationError:
# Let serializer validation report invalid values as 400 responses
return UserViewSet._BOOL_NOT_PROVIDED
def create(self, request, *args, **kwargs):
if not request.user.is_superuser and request.data.get("is_superuser") is True:
return HttpResponseForbidden(
"Superuser status can only be granted by a superuser",
)
requested_is_superuser = self._parse_requested_bool(
request.data,
"is_superuser",
)
requested_is_staff = self._parse_requested_bool(request.data, "is_staff")
if not request.user.is_superuser:
if requested_is_superuser is True:
return HttpResponseForbidden(
"Superuser status can only be granted by a superuser",
)
if requested_is_staff is True:
return HttpResponseForbidden(
"Staff status can only be granted by a superuser",
)
return super().create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
user_to_update: User = self.get_object()
if not request.user.is_superuser and user_to_update.is_superuser:
return HttpResponseForbidden(
"Superusers can only be modified by other superusers",
)
requested_is_superuser = self._parse_requested_bool(
request.data,
"is_superuser",
)
requested_is_staff = self._parse_requested_bool(request.data, "is_staff")
if (
not request.user.is_superuser
and request.data.get("is_superuser") is not None
and request.data.get("is_superuser") != user_to_update.is_superuser
and requested_is_superuser is not self._BOOL_NOT_PROVIDED
and requested_is_superuser != user_to_update.is_superuser
):
return HttpResponseForbidden(
"Superuser status can only be changed by a superuser",
)
if (
not request.user.is_superuser
and requested_is_staff is not self._BOOL_NOT_PROVIDED
and requested_is_staff != user_to_update.is_staff
):
return HttpResponseForbidden(
"Staff status can only be changed by a superuser",
)
return super().update(request, *args, **kwargs)
@extend_schema(

View File

@@ -472,7 +472,6 @@ class MailAccountHandler(LoggingMixin):
name=name,
defaults={
"match": name,
"matching_algorithm": Correspondent.MATCH_AUTO,
},
)[0]
except DatabaseError as e:

View File

@@ -448,7 +448,7 @@ class TestMail(
c = handler._get_correspondent(message, rule)
self.assertIsNotNone(c)
self.assertEqual(c.name, "someone@somewhere.com")
self.assertEqual(c.matching_algorithm, MatchingModel.MATCH_AUTO)
self.assertEqual(c.matching_algorithm, MatchingModel.MATCH_ANY)
self.assertEqual(c.match, "someone@somewhere.com")
c = handler._get_correspondent(message2, rule)
self.assertIsNotNone(c)

2
uv.lock generated
View File

@@ -1991,7 +1991,7 @@ wheels = [
[[package]]
name = "paperless-ngx"
version = "2.20.10"
version = "2.20.11"
source = { virtual = "." }
dependencies = [
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },