mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-14 13:47:58 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a167be5e1f | ||
|
|
4dc1736bee |
@@ -210,6 +210,9 @@ class MatchingModelSerializer(serializers.ModelSerializer[Any]):
|
|||||||
return match
|
return match
|
||||||
|
|
||||||
|
|
||||||
|
PERMISSION_ACTIONS = ("view", "change")
|
||||||
|
|
||||||
|
|
||||||
class SetPermissionsMixin:
|
class SetPermissionsMixin:
|
||||||
def _validate_user_ids(self, user_ids):
|
def _validate_user_ids(self, user_ids):
|
||||||
users = User.objects.none()
|
users = User.objects.none()
|
||||||
@@ -232,12 +235,9 @@ class SetPermissionsMixin:
|
|||||||
return groups
|
return groups
|
||||||
|
|
||||||
def validate_set_permissions(self, set_permissions=None):
|
def validate_set_permissions(self, set_permissions=None):
|
||||||
permissions_dict = {
|
permissions_dict = {action: {} for action in PERMISSION_ACTIONS}
|
||||||
"view": {},
|
|
||||||
"change": {},
|
|
||||||
}
|
|
||||||
if set_permissions is not None:
|
if set_permissions is not None:
|
||||||
for action in ["view", "change"]:
|
for action in PERMISSION_ACTIONS:
|
||||||
if action in set_permissions:
|
if action in set_permissions:
|
||||||
if "users" in set_permissions[action]:
|
if "users" in set_permissions[action]:
|
||||||
users = set_permissions[action]["users"]
|
users = set_permissions[action]["users"]
|
||||||
@@ -265,41 +265,31 @@ class SerializerWithPerms(serializers.Serializer[dict[str, Any]]):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_field(
|
class _PermissionSetSerializer(serializers.Serializer[dict[str, Any]]):
|
||||||
field={
|
users = serializers.ListField(
|
||||||
"type": "object",
|
child=serializers.IntegerField(),
|
||||||
"properties": {
|
required=False,
|
||||||
"view": {
|
allow_null=True,
|
||||||
"type": "object",
|
)
|
||||||
"properties": {
|
groups = serializers.ListField(
|
||||||
"users": {
|
child=serializers.IntegerField(),
|
||||||
"type": "array",
|
required=False,
|
||||||
"items": {"type": "integer"},
|
allow_null=True,
|
||||||
},
|
)
|
||||||
"groups": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "integer"},
|
class SetPermissionsSerializer(serializers.Serializer[dict[str, Any]]):
|
||||||
},
|
view = _PermissionSetSerializer(required=False)
|
||||||
},
|
change = _PermissionSetSerializer(required=False)
|
||||||
},
|
|
||||||
"change": {
|
def to_internal_value(self, data):
|
||||||
"type": "object",
|
if isinstance(data, dict):
|
||||||
"properties": {
|
unknown_keys = set(data) - set(PERMISSION_ACTIONS)
|
||||||
"users": {
|
if unknown_keys:
|
||||||
"type": "array",
|
raise serializers.ValidationError(
|
||||||
"items": {"type": "integer"},
|
{key: "Unknown permission action." for key in sorted(unknown_keys)},
|
||||||
},
|
)
|
||||||
"groups": {
|
return super().to_internal_value(data)
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "integer"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class SetPermissionsSerializer(serializers.DictField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OwnedObjectSerializer(
|
class OwnedObjectSerializer(
|
||||||
@@ -470,7 +460,6 @@ class OwnedObjectSerializer(
|
|||||||
|
|
||||||
set_permissions = SetPermissionsSerializer(
|
set_permissions = SetPermissionsSerializer(
|
||||||
label="Set permissions",
|
label="Set permissions",
|
||||||
allow_empty=True,
|
|
||||||
required=False,
|
required=False,
|
||||||
write_only=True,
|
write_only=True,
|
||||||
)
|
)
|
||||||
@@ -2045,8 +2034,13 @@ class BulkEditSerializer(
|
|||||||
def _validate_parameters_set_permissions(self, parameters) -> None:
|
def _validate_parameters_set_permissions(self, parameters) -> None:
|
||||||
if "set_permissions" not in parameters:
|
if "set_permissions" not in parameters:
|
||||||
raise serializers.ValidationError("set_permissions not specified")
|
raise serializers.ValidationError("set_permissions not specified")
|
||||||
|
set_permissions = parameters["set_permissions"]
|
||||||
|
if set_permissions is not None:
|
||||||
|
set_permissions = SetPermissionsSerializer().run_validation(
|
||||||
|
set_permissions,
|
||||||
|
)
|
||||||
parameters["set_permissions"] = self.validate_set_permissions(
|
parameters["set_permissions"] = self.validate_set_permissions(
|
||||||
parameters["set_permissions"],
|
set_permissions,
|
||||||
)
|
)
|
||||||
if "owner" in parameters and parameters["owner"] is not None:
|
if "owner" in parameters and parameters["owner"] is not None:
|
||||||
self._validate_owner(parameters["owner"])
|
self._validate_owner(parameters["owner"])
|
||||||
@@ -3001,9 +2995,8 @@ class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
permissions = serializers.DictField(
|
permissions = SetPermissionsSerializer(
|
||||||
label="Set permissions",
|
label="Set permissions",
|
||||||
allow_empty=False,
|
|
||||||
required=False,
|
required=False,
|
||||||
write_only=True,
|
write_only=True,
|
||||||
)
|
)
|
||||||
@@ -3039,8 +3032,8 @@ class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
|||||||
)
|
)
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
def _validate_permissions(self, permissions) -> None:
|
def _validate_permissions(self, permissions) -> dict:
|
||||||
self.validate_set_permissions(
|
return self.validate_set_permissions(
|
||||||
permissions,
|
permissions,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3064,7 +3057,11 @@ class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
|
|||||||
if operation == "set_permissions":
|
if operation == "set_permissions":
|
||||||
permissions = attrs.get("permissions")
|
permissions = attrs.get("permissions")
|
||||||
if permissions is not None:
|
if permissions is not None:
|
||||||
self._validate_permissions(permissions)
|
if not permissions:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"permissions must not be empty",
|
||||||
|
)
|
||||||
|
attrs["permissions"] = self._validate_permissions(permissions)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|||||||
@@ -1165,6 +1165,98 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertIn(b"set_permissions not specified", response.content)
|
self.assertIn(b"set_permissions not specified", response.content)
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
|
def test_set_permissions_rejects_non_dict_value(self, m) -> None:
|
||||||
|
self.setup_mock(m, "set_permissions")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "set_permissions",
|
||||||
|
"parameters": {"set_permissions": False},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"Expected a dictionary", response.content)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
|
def test_set_permissions_rejects_non_list_users(self, m) -> None:
|
||||||
|
self.setup_mock(m, "set_permissions")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "set_permissions",
|
||||||
|
"parameters": {
|
||||||
|
"set_permissions": {"view": {"users": False}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"Expected a list", response.content)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
|
def test_set_permissions_rejects_unknown_action(self, m) -> None:
|
||||||
|
self.setup_mock(m, "set_permissions")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "set_permissions",
|
||||||
|
"parameters": {
|
||||||
|
"set_permissions": {"not_a_real_action": {"users": [1]}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"Unknown permission action", response.content)
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
|
def test_set_permissions_null_is_a_noop(self, m) -> None:
|
||||||
|
"""
|
||||||
|
A `set_permissions: null` value is a deliberate no-op (e.g. a
|
||||||
|
request that only updates `owner`), not a validation error --
|
||||||
|
this must keep working even though every other non-dict value is
|
||||||
|
now rejected.
|
||||||
|
"""
|
||||||
|
self.setup_mock(m, "set_permissions")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "set_permissions",
|
||||||
|
"parameters": {
|
||||||
|
"set_permissions": None,
|
||||||
|
"owner": self.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||||
def test_set_permissions_merge(self, m) -> None:
|
def test_set_permissions_merge(self, m) -> None:
|
||||||
self.setup_mock(m, "set_permissions")
|
self.setup_mock(m, "set_permissions")
|
||||||
@@ -1300,7 +1392,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.client.force_authenticate(user=user1)
|
self.client.force_authenticate(user=user1)
|
||||||
|
|
||||||
permissions = {
|
permissions = {
|
||||||
"owner": user1.id,
|
"view": {"users": [user1.id]},
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
|||||||
@@ -1499,6 +1499,109 @@ class TestBulkEditObjectPermissions(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertTrue(Tag.objects.filter(pk=self.t1.id).exists())
|
self.assertTrue(Tag.objects.filter(pk=self.t1.id).exists())
|
||||||
|
|
||||||
|
def test_bulk_object_set_permissions_rejects_empty_permissions(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with set_permissions
|
||||||
|
operation and an empty permissions dict
|
||||||
|
THEN:
|
||||||
|
- Validation fails rather than silently applying a no-op
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
|
"permissions": {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_bulk_object_set_permissions_rejects_non_dict_permissions(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with set_permissions
|
||||||
|
operation and a non-dict permissions value
|
||||||
|
THEN:
|
||||||
|
- Validation fails rather than crashing
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
|
"permissions": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_bulk_object_set_permissions_rejects_unknown_action(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with set_permissions
|
||||||
|
operation and an unrecognized permission action name
|
||||||
|
THEN:
|
||||||
|
- Validation fails rather than silently no-oping
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
|
"permissions": {"not_a_real_action": {"users": [self.user1.id]}},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_bulk_object_set_permissions_null_users_is_a_noop(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing objects
|
||||||
|
WHEN:
|
||||||
|
- bulk_edit_objects API endpoint is called with set_permissions
|
||||||
|
operation and an explicit null for users/groups on an action
|
||||||
|
THEN:
|
||||||
|
- Request succeeds and is treated as "no users/groups for this
|
||||||
|
action", not a crash -- the normalized (id-checked) dict
|
||||||
|
returned by validate_set_permissions must actually be used,
|
||||||
|
not discarded in favor of the raw un-normalized input.
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/bulk_edit_objects/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"objects": [self.t1.id],
|
||||||
|
"object_type": "tags",
|
||||||
|
"operation": "set_permissions",
|
||||||
|
"permissions": {"view": {"users": None}},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
def test_bulk_edit_object_permissions_validation(self) -> None:
|
def test_bulk_edit_object_permissions_validation(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
Reference in New Issue
Block a user