mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-10 08:21:01 +00:00
Fix: enforce set_permissions shape via a nested DRF serializer (#14119)
SetPermissionsSerializer was a bare DictField, and the legacy bulk_edit set_permissions parameter bypassed even that by hand-calling validate_set_permissions() on an unchecked dict. A bool or a non-list in place of users/groups crashed with a raw TypeError instead of a 400. It is now a nested serializer (view/change, each with users/groups as lists of integers), used for owned-object create/update, the legacy bulk_edit set_permissions parameter, and bulk_edit_objects permissions. Unknown action keys are rejected: previously a typo like "veiw" was silently dropped, leaving an empty permission set that could clear existing grants. An explicit set_permissions null (an owner-only change) remains a no-op, and an empty bulk_edit_objects permissions dict is still rejected.
This commit is contained in:
1 parent
ff1e61b162
commit
ef26bc1570
5 files changed
+230
-67
No files matched your search
@@ -1165,6 +1165,77 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
self.assertIn(b"set_permissions not specified", response.content)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_set_permissions_rejects_malformed_set_permissions(self, m) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- A set_permissions bulk edit where set_permissions is not an
|
||||
object, has a non-list users value, or has an unknown action
|
||||
WHEN:
|
||||
- API to bulk edit is called
|
||||
THEN:
|
||||
- API returns HTTP 400 describing the problem
|
||||
- set_permissions is not called
|
||||
"""
|
||||
self.setup_mock(m, "set_permissions")
|
||||
|
||||
for set_permissions, expected_message in (
|
||||
(False, b"Expected a dictionary"),
|
||||
({"view": {"users": False}}, b"Expected a list"),
|
||||
({"not_a_real_action": {"users": [1]}}, b"Unknown permission action"),
|
||||
):
|
||||
with self.subTest(set_permissions=set_permissions):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_permissions",
|
||||
"parameters": {"set_permissions": set_permissions},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn(expected_message, 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:
|
||||
"""
|
||||
GIVEN:
|
||||
- A set_permissions bulk edit with set_permissions null and an
|
||||
owner, i.e. a request that only changes the owner
|
||||
WHEN:
|
||||
- API to bulk edit is called
|
||||
THEN:
|
||||
- Request succeeds
|
||||
- set_permissions receives no users or groups for any action
|
||||
"""
|
||||
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()
|
||||
self.assertEqual(
|
||||
m.call_args.kwargs["set_permissions"],
|
||||
{"view": {}, "change": {}},
|
||||
)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_set_permissions_merge(self, m) -> None:
|
||||
self.setup_mock(m, "set_permissions")
|
||||
@@ -1300,7 +1371,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
self.client.force_authenticate(user=user1)
|
||||
|
||||
permissions = {
|
||||
"owner": user1.id,
|
||||
"view": {"users": [user1.id]},
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
|
||||
@@ -1499,6 +1499,110 @@ class TestBulkEditObjectPermissions(APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
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_clears_users(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- An object a user has view permission on
|
||||
WHEN:
|
||||
- bulk_edit_objects API endpoint is called with set_permissions
|
||||
operation, merge off, and an explicit null for the view users
|
||||
THEN:
|
||||
- Request succeeds and null is treated as an empty user list,
|
||||
so the existing view permission is removed
|
||||
"""
|
||||
assign_perm("view_tag", self.user1, self.t1)
|
||||
|
||||
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)
|
||||
self.assertNotIn(self.user1, get_users_with_perms(self.t1))
|
||||
|
||||
def test_bulk_edit_object_permissions_validation(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -636,14 +636,9 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
|
||||
THEN:
|
||||
- Permission.DoesNotExist is raised, not a silent no-op
|
||||
|
||||
Regression test: the endpoint that calls this
|
||||
(BulkEditObjectPermissionsView) never actually validates action
|
||||
names against the raw client-supplied permissions dict --
|
||||
BulkEditObjectsSerializer._validate_permissions calls
|
||||
validate_set_permissions() only for its side-effecting user/group id
|
||||
checks and discards the filtered dict it returns -- so a bogus
|
||||
action key reaches this function as-is. Resolving the Permission via
|
||||
a bare `.filter()` (which returns empty instead of raising) would
|
||||
The API rejects unknown action names before they get here, but a
|
||||
direct caller could still pass one. Resolving the Permission via a
|
||||
bare `.filter()` (which returns empty instead of raising) would
|
||||
silently drop the grant and report success.
|
||||
"""
|
||||
with self.assertRaises(Permission.DoesNotExist):
|
||||
|
||||
Reference in new issue
Block a user