diff --git a/src-ui/cypress/e2e/documents/document-detail.cy.ts b/src-ui/cypress/e2e/documents/document-detail.cy.ts index dd5f8fac8..51d043ce3 100644 --- a/src-ui/cypress/e2e/documents/document-detail.cy.ts +++ b/src-ui/cypress/e2e/documents/document-detail.cy.ts @@ -5,11 +5,15 @@ describe('document-detail', () => { this.modifiedDocuments = [] cy.fixture('documents/documents.json').then((documentsJson) => { - cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { - let response = { ...documentsJson } - response = response.results.find((d) => d.id == 1) - req.reply(response) - }) + cy.intercept( + 'GET', + 'http://localhost:8000/api/documents/1/?full_perms=true', + (req) => { + let response = { ...documentsJson } + response = response.results.find((d) => d.id == 1) + req.reply(response) + } + ) }) cy.intercept('PUT', 'http://localhost:8000/api/documents/1/', (req) => { diff --git a/src-ui/cypress/fixtures/documents/documents.json b/src-ui/cypress/fixtures/documents/documents.json index e3938dba1..6b284f7b2 100644 --- a/src-ui/cypress/fixtures/documents/documents.json +++ b/src-ui/cypress/fixtures/documents/documents.json @@ -21,6 +21,7 @@ "original_file_name": "2022-03-22 no latin title.pdf", "archived_file_name": "2022-03-22 no latin title.pdf", "owner": null, + "user_can_change": true, "permissions": { "view": { "users": [], @@ -68,6 +69,7 @@ "original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf", "archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf", "owner": null, + "user_can_change": true, "permissions": { "view": { "users": [], @@ -98,6 +100,7 @@ "original_file_name": "2022-03-24 dolor.pdf", "archived_file_name": "2022-03-24 dolor.pdf", "owner": null, + "user_can_change": true, "permissions": { "view": { "users": [], @@ -128,6 +131,7 @@ "original_file_name": "2022-06-01 sit amet.pdf", "archived_file_name": "2022-06-01 sit amet.pdf", "owner": null, + "user_can_change": true, "permissions": { "view": { "users": [], diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index ec169fdd0..437acef90 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -122,7 +122,8 @@ export abstract class ManagementListComponent null, this.sortField, this.sortReverse, - this._nameFilter + this._nameFilter, + true ) .subscribe((c) => { this.data = c.results diff --git a/src-ui/src/app/data/object-with-permissions.ts b/src-ui/src/app/data/object-with-permissions.ts index 86f5f7394..9346aa85c 100644 --- a/src-ui/src/app/data/object-with-permissions.ts +++ b/src-ui/src/app/data/object-with-permissions.ts @@ -16,4 +16,6 @@ export interface ObjectWithPermissions extends ObjectWithId { owner?: number permissions?: PermissionsObject + + user_can_change?: boolean } diff --git a/src-ui/src/app/services/permissions.service.ts b/src-ui/src/app/services/permissions.service.ts index c19a4ee94..1e7b0d031 100644 --- a/src-ui/src/app/services/permissions.service.ts +++ b/src-ui/src/app/services/permissions.service.ts @@ -58,17 +58,24 @@ export class PermissionsService { action: string, object: ObjectWithPermissions ): boolean { - let actionObject = null - if (action === PermissionAction.View) actionObject = object.permissions.view - else if (action === PermissionAction.Change) - actionObject = object.permissions.change - if (!actionObject) return false - return ( - this.currentUserOwnsObject(object) || - actionObject.users.includes(this.currentUser.id) || - actionObject.groups.filter((g) => this.currentUser.groups.includes(g)) - .length > 0 - ) + if (action === PermissionAction.View) { + return ( + this.currentUserOwnsObject(object) || + object.permissions?.view.users.includes(this.currentUser.id) || + object.permissions?.view.groups.filter((g) => + this.currentUser.groups.includes(g) + ).length > 0 + ) + } else if (action === PermissionAction.Change) { + return ( + this.currentUserOwnsObject(object) || + object.user_can_change || + object.permissions?.change.users.includes(this.currentUser.id) || + object.permissions?.change.groups.filter((g) => + this.currentUser.groups.includes(g) + ).length > 0 + ) + } } public getPermissionCode( diff --git a/src-ui/src/app/services/rest/abstract-name-filter-service.ts b/src-ui/src/app/services/rest/abstract-name-filter-service.ts index 568803fb8..0b5fca835 100644 --- a/src-ui/src/app/services/rest/abstract-name-filter-service.ts +++ b/src-ui/src/app/services/rest/abstract-name-filter-service.ts @@ -9,11 +9,15 @@ export abstract class AbstractNameFilterService< pageSize?: number, sortField?: string, sortReverse?: boolean, - nameFilter?: string + nameFilter?: string, + fullPerms?: boolean ) { let params = {} if (nameFilter) { - params = { name__icontains: nameFilter } + params['name__icontains'] = nameFilter + } + if (fullPerms) { + params['full_perms'] = true } return this.list(page, pageSize, sortField, sortReverse, params) } diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 63b447b9a..4ff2ee88f 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -113,6 +113,14 @@ export class DocumentService extends AbstractPaperlessService }).pipe(map((response) => response.results.map((doc) => doc.id))) } + get(id: number): Observable { + return this.http.get(this.getResourceUrl(id), { + params: { + full_perms: true, + }, + }) + } + getPreviewUrl(id: number, original: boolean = false): string { let url = this.getResourceUrl(id, 'preview') if (this._searchQuery) url += `#search="${this._searchQuery}"` diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 580ff11c9..ad2b5d0f6 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -14,6 +14,7 @@ from django.contrib.auth.models import Group from django.contrib.auth.models import User from django.utils.text import slugify from django.utils.translation import gettext as _ +from guardian.core import ObjectPermissionChecker from guardian.shortcuts import get_users_with_perms from rest_framework import serializers from rest_framework.fields import SerializerMethodField @@ -149,11 +150,21 @@ class SetPermissionsMixin: class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) + full_perms = kwargs.pop("full_perms", False) super().__init__(*args, **kwargs) + try: + if full_perms: + self.fields.pop("user_can_change") + else: + self.fields.pop("permissions") + except KeyError: + pass + def get_permissions(self, obj): view_codename = f"view_{obj.__class__.__name__.lower()}" change_codename = f"change_{obj.__class__.__name__.lower()}" + return { "view": { "users": get_users_with_perms( @@ -179,7 +190,19 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin): }, } + def get_user_can_change(self, obj): + checker = ObjectPermissionChecker(self.user) if self.user is not None else None + return ( + obj.owner is None + or obj.owner == self.user + or ( + self.user is not None + and checker.has_perm(f"change_{obj.__class__.__name__.lower()}", obj) + ) + ) + permissions = SerializerMethodField(read_only=True) + user_can_change = SerializerMethodField(read_only=True) set_permissions = serializers.DictField( label="Set permissions", @@ -235,6 +258,7 @@ class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): "last_correspondence", "owner", "permissions", + "user_can_change", "set_permissions", ) @@ -252,6 +276,7 @@ class DocumentTypeSerializer(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", + "user_can_change", "set_permissions", ) @@ -303,6 +328,7 @@ class TagSerializerVersion1(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", + "user_can_change", "set_permissions", ) @@ -338,6 +364,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", + "user_can_change", "set_permissions", ) @@ -437,6 +464,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer): "archived_file_name", "owner", "permissions", + "user_can_change", "set_permissions", "notes", ) @@ -464,6 +492,7 @@ class SavedViewSerializer(OwnedObjectSerializer): "filter_rules", "owner", "permissions", + "user_can_change", "set_permissions", ] @@ -783,6 +812,7 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", + "user_can_change", "set_permissions", ) diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index 8b81068a2..a6307e2d5 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -3396,6 +3396,36 @@ class TestApiAuth(DirectoriesMixin, APITestCase): status.HTTP_404_NOT_FOUND, ) + def test_dynamic_permissions_fields(self): + Document.objects.create(title="Test", content="content 1", checksum="1") + + user1 = User.objects.create_superuser(username="test1") + self.client.force_authenticate(user1) + + response = self.client.get( + "/api/documents/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertNotIn("permissions", resp_data["results"][0]) + self.assertIn("user_can_change", resp_data["results"][0]) + + response = self.client.get( + "/api/documents/?full_perms=true", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertIn("permissions", resp_data["results"][0]) + self.assertNotIn("user_can_change", resp_data["results"][0]) + class TestApiRemoteVersion(DirectoriesMixin, APITestCase): ENDPOINT = "/api/remote_version/" diff --git a/src/documents/views.py b/src/documents/views.py index e178fa2d5..234c4dda1 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -157,6 +157,10 @@ class PassUserMixin(CreateModelMixin): def get_serializer(self, *args, **kwargs): kwargs.setdefault("user", self.request.user) + kwargs.setdefault( + "full_perms", + self.request.query_params.get("full_perms", False), + ) return super().get_serializer(*args, **kwargs) @@ -274,6 +278,10 @@ class DocumentViewSet( kwargs.setdefault("context", self.get_serializer_context()) kwargs.setdefault("fields", fields) kwargs.setdefault("truncate_content", truncate_content.lower() in ["true", "1"]) + kwargs.setdefault( + "full_perms", + self.request.query_params.get("full_perms", False), + ) return serializer_class(*args, **kwargs) def update(self, request, *args, **kwargs):