]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Unify API perm endpoint to `set_permissions`, initial frontend support for doc sharing
authorMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Wed, 7 Dec 2022 08:36:31 +0000 (00:36 -0800)
committerMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Wed, 7 Dec 2022 08:49:26 +0000 (00:49 -0800)
12 files changed:
src-ui/src/app/components/common/input/select/select.component.html
src-ui/src/app/components/common/input/select/select.component.ts
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/components/document-detail/document-detail.component.ts
src-ui/src/app/data/matching-model.ts
src-ui/src/app/data/object-with-permissions.ts [new file with mode: 0644]
src-ui/src/app/data/paperless-document.ts
src-ui/src/app/services/rest/abstract-paperless-service.ts
src/documents/serialisers.py
src/documents/tests/test_api.py
src/documents/views.py
src/paperless/views.py

index 83e642bed45599421bcf532df9ad3556997fe280..d775f4ffa45517e990aa4e87e19a08942b65081b 100644 (file)
@@ -12,7 +12,7 @@
         i18n-addTagText="Used for both types, correspondents, storage paths"
         [placeholder]="placeholder"
         [multiple]="multiple"
-        bindLabel="name"
+        [bindLabel]="bindLabel"
         bindValue="id"
         (change)="onChange(value)"
         (search)="onSearch($event)"
index 9ae361387ded862e44437432270386de51277f69..877b0f78da686726d7f8fa8a6413e22890d54b10 100644 (file)
@@ -47,6 +47,9 @@ export class SelectComponent extends AbstractInputComponent<number> {
   @Input()
   multiple: boolean = false
 
+  @Input()
+  bindLabel: string = 'name'
+
   @Output()
   createNew = new EventEmitter<string>()
 
index 509e1b87404b8ca6d187f4815fb715b1f0439b87..9ffc00dd41e53a4319330279f3be34c72e27fa97 100644 (file)
                         </div>
                     </ng-template>
                 </li>
+
                 <li [ngbNavItem]="5" *ngIf="commentsEnabled">
                     <a ngbNavLink i18n>Comments</a>
                     <ng-template ngbNavContent>
                         <app-document-comments [documentId]="documentId"></app-document-comments>
                     </ng-template>
                 </li>
+
+                <li [ngbNavItem]="6">
+                    <a ngbNavLink i18n>Permissions</a>
+                    <ng-template ngbNavContent>
+                        <div formGroupName="set_permissions">
+                            <app-input-select i18n-title title="Users can view" [items]="users" [bindLabel]="'username'" multiple="true" formControlName="view"></app-input-select>
+                            <app-input-select i18n-title title="Users can edit" [items]="users" [bindLabel]="'username'" multiple="true" formControlName="change"></app-input-select>
+                        </div>
+                    </ng-template>
+                </li>
             </ul>
 
             <div [ngbNavOutlet]="nav" class="mt-2"></div>
index 624ea7720fc36a0592663b70e5987027a619b033..72f4be70b6f4638e71ac29a08e81da16c3122b55 100644 (file)
@@ -40,6 +40,8 @@ import {
   PermissionsService,
   PermissionType,
 } from 'src/app/services/permissions.service'
+import { UserService } from 'src/app/services/rest/user.service'
+import { PaperlessUser } from 'src/app/data/paperless-user'
 
 @Component({
   selector: 'app-document-detail',
@@ -73,6 +75,7 @@ export class DocumentDetailComponent
   correspondents: PaperlessCorrespondent[]
   documentTypes: PaperlessDocumentType[]
   storagePaths: PaperlessStoragePath[]
+  users: PaperlessUser[]
 
   documentForm: FormGroup = new FormGroup({
     title: new FormControl(''),
@@ -83,6 +86,10 @@ export class DocumentDetailComponent
     storage_path: new FormControl(),
     archive_serial_number: new FormControl(),
     tags: new FormControl([]),
+    set_permissions: new FormGroup({
+      view: new FormControl(null),
+      change: new FormControl(null),
+    }),
   })
 
   previewCurrentPage: number = 1
@@ -127,7 +134,8 @@ export class DocumentDetailComponent
     private toastService: ToastService,
     private settings: SettingsService,
     private storagePathService: StoragePathService,
-    private permissionsService: PermissionsService
+    private permissionsService: PermissionsService,
+    private userService: UserService
   ) {}
 
   titleKeyUp(event) {
@@ -167,6 +175,11 @@ export class DocumentDetailComponent
       .pipe(first())
       .subscribe((result) => (this.storagePaths = result.results))
 
+    this.userService
+      .listAll()
+      .pipe(first())
+      .subscribe((result) => (this.users = result.results))
+
     this.route.paramMap
       .pipe(
         takeUntil(this.unsubscribeNotifier),
@@ -230,6 +243,14 @@ export class DocumentDetailComponent
             storage_path: doc.storage_path,
             archive_serial_number: doc.archive_serial_number,
             tags: [...doc.tags],
+            set_permissions: {
+              view: doc.permissions
+                .filter((p) => (p[1] as string).includes('view'))
+                .map((p) => p[0]),
+              change: doc.permissions
+                .filter((p) => (p[1] as string).includes('change'))
+                .map((p) => p[0]),
+            },
           })
 
           this.isDirty$ = dirtyCheck(
@@ -284,6 +305,14 @@ export class DocumentDetailComponent
         },
       })
     this.title = this.documentTitlePipe.transform(doc.title)
+    doc['set_permissions'] = {
+      view: doc.permissions
+        .filter((p) => (p[1] as string).includes('view'))
+        .map((p) => p[0]),
+      change: doc.permissions
+        .filter((p) => (p[1] as string).includes('change'))
+        .map((p) => p[0]),
+    }
     this.documentForm.patchValue(doc)
   }
 
@@ -376,7 +405,7 @@ export class DocumentDetailComponent
       .update(this.document)
       .pipe(first())
       .subscribe({
-        next: (result) => {
+        next: () => {
           this.close()
           this.networkActive = false
           this.error = null
index 8ce05528e401dbcc0fa9f2705c7657cbef56f108..387625b547cb618140554fdd0db3efb5c794db38 100644 (file)
@@ -1,4 +1,4 @@
-import { ObjectWithId } from './object-with-id'
+import { ObjectWithPermissions } from './object-with-permissions'
 
 export const MATCH_ANY = 1
 export const MATCH_ALL = 2
@@ -41,7 +41,7 @@ export const MATCHING_ALGORITHMS = [
   },
 ]
 
-export interface MatchingModel extends ObjectWithId {
+export interface MatchingModel extends ObjectWithPermissions {
   name?: string
 
   slug?: string
diff --git a/src-ui/src/app/data/object-with-permissions.ts b/src-ui/src/app/data/object-with-permissions.ts
new file mode 100644 (file)
index 0000000..786cb16
--- /dev/null
@@ -0,0 +1,8 @@
+import { ObjectWithId } from './object-with-id'
+import { PaperlessUser } from './paperless-user'
+
+export interface ObjectWithPermissions extends ObjectWithId {
+  user?: PaperlessUser
+
+  permissions?: Array<[number, string]>
+}
index 8b038d79e72eb70623456fc54b9b12a1ff38fa27..9899c60aca0b2b68e634ef46c55de702d2c8cea3 100644 (file)
@@ -1,9 +1,9 @@
 import { PaperlessCorrespondent } from './paperless-correspondent'
-import { ObjectWithId } from './object-with-id'
 import { PaperlessTag } from './paperless-tag'
 import { PaperlessDocumentType } from './paperless-document-type'
 import { Observable } from 'rxjs'
 import { PaperlessStoragePath } from './paperless-storage-path'
+import { ObjectWithPermissions } from './object-with-permissions'
 
 export interface SearchHit {
   score?: number
@@ -12,7 +12,7 @@ export interface SearchHit {
   highlights?: string
 }
 
-export interface PaperlessDocument extends ObjectWithId {
+export interface PaperlessDocument extends ObjectWithPermissions {
   correspondent$?: Observable<PaperlessCorrespondent>
 
   correspondent?: number
index 9a5664c9d2c4de6410697ee125bfba9f679cd648..f7833c81225b1463448674055e74842c943d01fd 100644 (file)
@@ -2,8 +2,10 @@ import { HttpClient, HttpParams } from '@angular/common/http'
 import { Observable } from 'rxjs'
 import { map, publishReplay, refCount } from 'rxjs/operators'
 import { ObjectWithId } from 'src/app/data/object-with-id'
+import { PaperlessUser } from 'src/app/data/paperless-user'
 import { Results } from 'src/app/data/results'
 import { environment } from 'src/environments/environment'
+import { PermissionAction, PermissionType } from '../permissions.service'
 
 export abstract class AbstractPaperlessService<T extends ObjectWithId> {
   protected baseUrl: string = environment.apiBaseUrl
index 234ef21daecf5eeae3a9d95400d02619b5967947..43f3a758e3630e374b3df976688c10fea1a69b57 100644 (file)
@@ -31,6 +31,7 @@ from .parsers import is_mime_type_supported
 from guardian.models import UserObjectPermission
 from guardian.shortcuts import assign_perm
 from guardian.shortcuts import remove_perm
+from guardian.shortcuts import get_users_with_perms
 
 from django.contrib.contenttypes.models import ContentType
 
@@ -91,55 +92,36 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
         ).values_list("user", "permission__codename")
         return list(user_object_perms)
 
-    permissions = SerializerMethodField()
+    permissions = SerializerMethodField(read_only=True)
 
-    grant_permissions = serializers.DictField(
-        label="Grant permissions",
+    set_permissions = serializers.DictField(
+        label="Set permissions",
         allow_empty=True,
         required=False,
         write_only=True,
     )
 
     def _validate_user_ids(self, user_ids):
-        users = User.objects.filter(id__in=user_ids)
-        if not users.count() == len(users):
-            raise serializers.ValidationError(
-                "Some users in don't exist or were specified twice.",
-            )
+        users = User.objects.none()
+        if user_ids is not None:
+            users = User.objects.filter(id__in=user_ids)
+            if not users.count() == len(user_ids):
+                raise serializers.ValidationError(
+                    "Some users in don't exist or were specified twice.",
+                )
         return users
 
-    def validate_grant_permissions(self, grant_permissions):
-        user_dict = {
-            "view": User.objects.none(),
-            "change": User.objects.none(),
-        }
-        if grant_permissions is not None:
-            if "view" in grant_permissions:
-                view_list = grant_permissions["view"]
-                user_dict["view"] = self._validate_user_ids(view_list)
-            if "change" in grant_permissions:
-                change_list = grant_permissions["change"]
-                user_dict["change"] = self._validate_user_ids(change_list)
-        return user_dict
-
-    revoke_permissions = serializers.DictField(
-        label="Revoke permissions",
-        allow_empty=True,
-        required=False,
-        write_only=True,
-    )
-
-    def validate_revoke_permissions(self, revoke_permissions):
+    def validate_set_permissions(self, set_permissions):
         user_dict = {
             "view": User.objects.none(),
             "change": User.objects.none(),
         }
-        if revoke_permissions is not None:
-            if "view" in revoke_permissions:
-                view_list = revoke_permissions["view"]
+        if set_permissions is not None:
+            if "view" in set_permissions:
+                view_list = set_permissions["view"]
                 user_dict["view"] = self._validate_user_ids(view_list)
-            if "change" in revoke_permissions:
-                change_list = revoke_permissions["change"]
+            if "change" in set_permissions:
+                change_list = set_permissions["change"]
                 user_dict["change"] = self._validate_user_ids(change_list)
         return user_dict
 
@@ -147,21 +129,25 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
         self.user = kwargs.pop("user", None)
         return super().__init__(*args, **kwargs)
 
-    def _adjust_permissions(self, users, object, type="view", grant=True):
-        if grant:
-            for user in users:
-                assign_perm(
-                    f"{type}_{object.__class__.__name__.lower()}",
-                    user,
-                    object,
-                )
-        else:
-            for user in users:
-                remove_perm(
-                    f"{type}_{object.__class__.__name__.lower()}",
-                    user,
-                    object,
-                )
+    def _set_permissions(self, permissions, object):
+        for action in permissions:
+            permission = f"{action}_{object.__class__.__name__.lower()}"
+            users_to_add = permissions[action]
+            users_to_remove = get_users_with_perms(
+                object,
+                only_with_perms_in=[permission],
+            ).difference(users_to_add)
+            for user in users_to_remove:
+                remove_perm(permission, user, object)
+            for user in users_to_add:
+                assign_perm(permission, user, object)
+                if action == "change":
+                    # change gives view too
+                    assign_perm(
+                        f"view_{object.__class__.__name__.lower()}",
+                        user,
+                        object,
+                    )
 
     def create(self, validated_data):
         if self.user and (
@@ -169,55 +155,13 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
         ):
             validated_data["owner"] = self.user
         instance = super().create(validated_data)
-        if "grant_permissions" in validated_data:
-            self._adjust_permissions(
-                validated_data["grant_permissions"]["view"],
-                instance,
-            )
-            self._adjust_permissions(
-                validated_data["grant_permissions"]["change"],
-                instance,
-                "change",
-            )
-        if "revoke_permissions" in validated_data:
-            self._adjust_permissions(
-                validated_data["revoke_permissions"]["view"],
-                instance,
-                "view",
-                False,
-            )
-            self._adjust_permissions(
-                validated_data["revoke_permissions"]["change"],
-                instance,
-                "change",
-                False,
-            )
+        if "set_permissions" in validated_data:
+            self._set_permissions(validated_data["set_permissions"], instance)
         return instance
 
     def update(self, instance, validated_data):
-        if "grant_permissions" in validated_data:
-            self._adjust_permissions(
-                validated_data["grant_permissions"]["view"],
-                instance,
-            )
-            self._adjust_permissions(
-                validated_data["grant_permissions"]["change"],
-                instance,
-                "change",
-            )
-        if "revoke_permissions" in validated_data:
-            self._adjust_permissions(
-                validated_data["revoke_permissions"]["view"],
-                instance,
-                "view",
-                False,
-            )
-            self._adjust_permissions(
-                validated_data["revoke_permissions"]["change"],
-                instance,
-                "change",
-                False,
-            )
+        if "set_permissions" in validated_data:
+            self._set_permissions(validated_data["set_permissions"], instance)
         return super().update(instance, validated_data)
 
 
@@ -238,8 +182,7 @@ class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
             "last_correspondence",
             "owner",
             "permissions",
-            "grant_permissions",
-            "revoke_permissions",
+            "set_permissions",
         )
 
 
@@ -256,8 +199,7 @@ class DocumentTypeSerializer(MatchingModelSerializer, OwnedObjectSerializer):
             "document_count",
             "owner",
             "permissions",
-            "grant_permissions",
-            "revoke_permissions",
+            "set_permissions",
         )
 
 
@@ -342,8 +284,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
             "document_count",
             "owner",
             "permissions",
-            "grant_permissions",
-            "revoke_permissions",
+            "set_permissions",
         )
 
     def validate_color(self, color):
@@ -426,8 +367,7 @@ class DocumentSerializer(DynamicFieldsModelSerializer, OwnedObjectSerializer):
             "archived_file_name",
             "owner",
             "permissions",
-            "grant_permissions",
-            "revoke_permissions",
+            "set_permissions",
         )
 
 
@@ -454,8 +394,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
             "filter_rules",
             "owner",
             "permissions",
-            "grant_permissions",
-            "revoke_permissions",
+            "set_permissions",
         ]
 
     def update(self, instance, validated_data):
@@ -749,8 +688,7 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer):
             "document_count",
             "owner",
             "permissions",
-            "grant_permissions",
-            "revoke_permissions",
+            "set_permissions",
         )
 
     def validate_path(self, path):
index 2b937867c8af0d28b4b2367f4d064af0ea98efc5..402c1023f10c655efb2ebc5283b29e7921e66c24 100644 (file)
@@ -3015,7 +3015,7 @@ class TestApiUser(APITestCase):
         response = self.client.get(self.ENDPOINT)
 
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data["count"], 3)  # AnonymousUser
+        self.assertEqual(response.data["count"], 2)
         returned_user2 = response.data["results"][2]
 
         self.assertEqual(returned_user2["username"], user1.username)
index 8af4e14775d436eea5f3336b97213d0896f73b3b..8b1dfedde598179fdeb74f274c6e11df5ccc0a4b 100644 (file)
@@ -256,6 +256,7 @@ class DocumentViewSet(
         else:
             fields = None
         serializer_class = self.get_serializer_class()
+        kwargs.setdefault("user", self.request.user)  # PassUserMixin
         kwargs.setdefault("context", self.get_serializer_context())
         kwargs.setdefault("fields", fields)
         return serializer_class(*args, **kwargs)
index 7ff1462d3a0632ebfc1fbd1b8074fc02ba7ef2bb..fea2d7bf5de071043c84ed202a8137df02dc9d99 100644 (file)
@@ -39,7 +39,9 @@ class FaviconView(View):
 class UserViewSet(ModelViewSet):
     model = User
 
-    queryset = User.objects.exclude(username="consumer").order_by(Lower("username"))
+    queryset = User.objects.exclude(
+        username__in=["consumer", "AnonymousUser"],
+    ).order_by(Lower("username"))
 
     serializer_class = UserSerializer
     pagination_class = StandardPagination