]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Refactor permissions API endpoints, UI group permissions
authorMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Thu, 8 Dec 2022 05:11:47 +0000 (21:11 -0800)
committerMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Thu, 8 Dec 2022 08:05:16 +0000 (00:05 -0800)
29 files changed:
src-ui/src/app/app.component.ts
src-ui/src/app/app.module.ts
src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts
src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts
src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts
src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts
src-ui/src/app/components/common/input/permissions-group/permissions-group.component.html [new file with mode: 0644]
src-ui/src/app/components/common/input/permissions-group/permissions-group.component.scss [moved from src-ui/src/app/components/common/input/share-user/share-user.component.scss with 100% similarity]
src-ui/src/app/components/common/input/permissions-group/permissions-group.component.ts [new file with mode: 0644]
src-ui/src/app/components/common/input/permissions-user/permissions-user.component.html [moved from src-ui/src/app/components/common/input/share-user/share-user.component.html with 100% similarity]
src-ui/src/app/components/common/input/permissions-user/permissions-user.component.scss [new file with mode: 0644]
src-ui/src/app/components/common/input/permissions-user/permissions-user.component.ts [moved from src-ui/src/app/components/common/input/share-user/share-user.component.ts with 63% similarity]
src-ui/src/app/components/common/permissions-select/permissions-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/components/manage/management-list/management-list.component.ts
src-ui/src/app/data/object-with-permissions.ts
src-ui/src/app/directives/if-object-permissions.directive.ts
src-ui/src/app/directives/if-owner.directive.ts
src-ui/src/app/directives/if-permissions.directive.ts
src-ui/src/app/guards/permissions.guard.ts
src-ui/src/app/services/permissions.service.ts
src/documents/serialisers.py
src/documents/tests/test_api.py

index 320e9393ab790ae8c3f1a46d7fbe273b3a4af15b..01eac1297ab9f7cf2b807bc0a7db356c10394ce4 100644 (file)
@@ -81,10 +81,10 @@ export class AppComponent implements OnInit, OnDestroy {
           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
         ) {
           if (
-            this.permissionsService.currentUserCan({
-              action: PermissionAction.View,
-              type: PermissionType.Document,
-            })
+            this.permissionsService.currentUserCan(
+              PermissionAction.View,
+              PermissionType.Document
+            )
           ) {
             this.toastService.show({
               title: $localize`Document added`,
@@ -246,10 +246,10 @@ export class AppComponent implements OnInit, OnDestroy {
   public get dragDropEnabled(): boolean {
     return (
       !this.router.url.includes('dashboard') &&
-      this.permissionsService.currentUserCan({
-        action: PermissionAction.Add,
-        type: PermissionType.Document,
-      })
+      this.permissionsService.currentUserCan(
+        PermissionAction.Add,
+        PermissionType.Document
+      )
     )
   }
 
index 60a6f4844ee22d448f41777575df9693637c91b5..9e97ac90b67f59282c2e98009146e6d10261a7c0 100644 (file)
@@ -84,6 +84,10 @@ import { GroupEditDialogComponent } from './components/common/edit-dialog/group-
 import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
 import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
 import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
+import { PermissionsUserComponent } from './components/common/input/permissions-user/permissions-user.component'
+import { PermissionsGroupComponent } from './components/common/input/permissions-group/permissions-group.component'
+import { IfOwnerDirective } from './directives/if-owner.directive'
+import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
 
 import localeBe from '@angular/common/locales/be'
 import localeCs from '@angular/common/locales/cs'
@@ -104,9 +108,6 @@ import localeSr from '@angular/common/locales/sr'
 import localeSv from '@angular/common/locales/sv'
 import localeTr from '@angular/common/locales/tr'
 import localeZh from '@angular/common/locales/zh'
-import { ShareUserComponent } from './components/common/input/share-user/share-user.component'
-import { IfOwnerDirective } from './directives/if-owner.directive'
-import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
 
 registerLocaleData(localeBe)
 registerLocaleData(localeCs)
@@ -198,7 +199,8 @@ function initializeApp(settings: SettingsService) {
     PermissionsSelectComponent,
     MailAccountEditDialogComponent,
     MailRuleEditDialogComponent,
-    ShareUserComponent,
+    PermissionsUserComponent,
+    PermissionsGroupComponent,
     IfOwnerDirective,
     IfObjectPermissionsDirective,
   ],
index 83f24812f5ff858b59901f1245ead71ea94a590b..3867146a316c3ab47f9814001ee454f70fc06997 100644 (file)
     <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
     <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
 
-    <div *ifOwner="object?.owner">
+    <div *ifOwner="object">
       <h5 i18n>Permissions</h5>
       <div formGroupName="set_permissions">
-        <app-share-user type="view" formControlName="view"></app-share-user>
-        <app-share-user type="change" formControlName="change"></app-share-user>
+        <h6 i18n>View</h6>
+        <div formGroupName="view">
+          <app-permissions-user type="view" formControlName="users"></app-permissions-user>
+          <app-permissions-group type="view" formControlName="groups"></app-permissions-group>
+        </div>
+        <h6 i18n>Edit</h6>
+        <div formGroupName="change">
+          <app-permissions-user type="change" formControlName="users"></app-permissions-user>
+          <app-permissions-group type="change" formControlName="groups"></app-permissions-group>
+        </div>
       </div>
     </div>
 
index a7c3eb60673ef8dd8d08da06a93cafbb10f6785c..5320def273e78219d2fcb8c2455cf98bcad03af0 100644 (file)
@@ -31,8 +31,14 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
       match: new FormControl(''),
       is_insensitive: new FormControl(true),
       set_permissions: new FormGroup({
-        view: new FormControl(null),
-        change: new FormControl(null),
+        view: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
+        change: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
       }),
     })
   }
index 3aa196f31e85407be5daa64044c15ad1e196cb9e..72a41408b5fc192ee322578250bbc14ea425c3fc 100644 (file)
       <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
       <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
 
-      <div *ifOwner="object?.owner">
+      <div *ifOwner="object">
         <h5 i18n>Permissions</h5>
         <div formGroupName="set_permissions">
-          <app-share-user type="view" formControlName="view"></app-share-user>
-          <app-share-user type="change" formControlName="change"></app-share-user>
+          <h6 i18n>View</h6>
+          <div formGroupName="view">
+            <app-permissions-user type="view" formControlName="users"></app-permissions-user>
+            <app-permissions-group type="view" formControlName="groups"></app-permissions-group>
+          </div>
+          <h6 i18n>Edit</h6>
+          <div formGroupName="change">
+            <app-permissions-user type="change" formControlName="users"></app-permissions-user>
+            <app-permissions-group type="change" formControlName="groups"></app-permissions-group>
+          </div>
         </div>
       </div>
 
index ef4d0a864dda7e32f1c28b0caf8d9d45394f4de3..81854df34264253ef7873103f19637717b7b3b83 100644 (file)
@@ -31,8 +31,14 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
       match: new FormControl(''),
       is_insensitive: new FormControl(true),
       set_permissions: new FormGroup({
-        view: new FormControl(null),
-        change: new FormControl(null),
+        view: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
+        change: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
       }),
     })
   }
index a9133f60f9e1f03fc16252a5b0154068105bc0b9..7c23149f6a6bdae31db3a076298eb8e642bee56b 100644 (file)
@@ -39,14 +39,7 @@ export abstract class EditDialogComponent<
   ngOnInit(): void {
     if (this.object != null) {
       if (this.object['permissions']) {
-        this.object['set_permissions'] = {
-          view: (this.object as ObjectWithPermissions).permissions
-            .filter((p) => (p[1] as string).includes('view'))
-            .map((p) => p[0]),
-          change: (this.object as ObjectWithPermissions).permissions
-            .filter((p) => (p[1] as string).includes('change'))
-            .map((p) => p[0]),
-        }
+        this.object['set_permissions'] = this.object['permissions']
       }
       this.objectForm.patchValue(this.object)
     }
index a0e141907bba92e3010880c9a9eb2afa345e0238..24379a52dfdb43e1fd1a8d5abbbc815261a9bd4e 100644 (file)
     <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
     <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
 
-    <div *ifOwner="object?.owner">
+    <div *ifOwner="object">
       <h5 i18n>Permissions</h5>
       <div formGroupName="set_permissions">
-        <app-share-user type="view" formControlName="view"></app-share-user>
-        <app-share-user type="change" formControlName="change"></app-share-user>
+        <h6 i18n>View</h6>
+        <div formGroupName="view">
+          <app-permissions-user type="view" formControlName="users"></app-permissions-user>
+          <app-permissions-group type="view" formControlName="groups"></app-permissions-group>
+        </div>
+        <h6 i18n>Edit</h6>
+        <div formGroupName="change">
+          <app-permissions-user type="change" formControlName="users"></app-permissions-user>
+          <app-permissions-group type="change" formControlName="groups"></app-permissions-group>
+        </div>
       </div>
     </div>
 
index 7c4898703b9790098e07586d605f9fea52b63334..42d533c961681f71038efcb0c17cd298dcb275f6 100644 (file)
@@ -42,8 +42,14 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
       match: new FormControl(''),
       is_insensitive: new FormControl(true),
       set_permissions: new FormGroup({
-        view: new FormControl(null),
-        change: new FormControl(null),
+        view: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
+        change: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
       }),
     })
   }
index 4e6d0e22ffcef450e9551493e686b5c43eaf2ba1..a98b2a4dde291ec40fb68ffa7c4516f2f6e08cc1 100644 (file)
       <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
       <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
 
-      <div *ifOwner="object?.owner">
+      <div *ifOwner="object">
         <h5 i18n>Permissions</h5>
         <div formGroupName="set_permissions">
-          <app-share-user type="view" formControlName="view"></app-share-user>
-          <app-share-user type="change" formControlName="change"></app-share-user>
+          <h6 i18n>View</h6>
+          <div formGroupName="view">
+            <app-permissions-user type="view" formControlName="users"></app-permissions-user>
+            <app-permissions-group type="view" formControlName="groups"></app-permissions-group>
+          </div>
+          <h6 i18n>Edit</h6>
+          <div formGroupName="change">
+            <app-permissions-user type="change" formControlName="users"></app-permissions-user>
+            <app-permissions-group type="change" formControlName="groups"></app-permissions-group>
+          </div>
         </div>
       </div>
 
index 0414052a06ac03291a64caddf5f971442c9dece6..8b79d36bc8f8c5052daf9372387c5c67f9fb89c4 100644 (file)
@@ -34,8 +34,14 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
       match: new FormControl(''),
       is_insensitive: new FormControl(true),
       set_permissions: new FormGroup({
-        view: new FormControl(null),
-        change: new FormControl(null),
+        view: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
+        change: new FormGroup({
+          users: new FormControl(null),
+          groups: new FormControl(null),
+        }),
       }),
     })
   }
diff --git a/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.html b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.html
new file mode 100644 (file)
index 0000000..1b74b1e
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="mb-3 paperless-input-select">
+    <label class="form-label" [for]="inputId">{{title}}</label>
+      <div>
+        <ng-select name="inputId" [(ngModel)]="value"
+          [disabled]="disabled"
+          clearable="true"
+          [items]="groups"
+          multiple="true"
+          bindLabel="name"
+          bindValue="id"
+          (change)="onChange(value)">
+        </ng-select>
+      </div>
+    <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
+  </div>
diff --git a/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.ts b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.ts
new file mode 100644 (file)
index 0000000..bef1727
--- /dev/null
@@ -0,0 +1,48 @@
+import { Component, forwardRef, Input, OnInit } from '@angular/core'
+import { NG_VALUE_ACCESSOR } from '@angular/forms'
+import { first } from 'rxjs/operators'
+import { PaperlessGroup } from 'src/app/data/paperless-group'
+import { GroupService } from 'src/app/services/rest/group.service'
+import { SettingsService } from 'src/app/services/settings.service'
+import { AbstractInputComponent } from '../abstract-input'
+
+@Component({
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => PermissionsGroupComponent),
+      multi: true,
+    },
+  ],
+  selector: 'app-permissions-group',
+  templateUrl: './permissions-group.component.html',
+  styleUrls: ['./permissions-group.component.scss'],
+})
+export class PermissionsGroupComponent
+  extends AbstractInputComponent<PaperlessGroup>
+  implements OnInit
+{
+  groups: PaperlessGroup[]
+
+  @Input()
+  type: string
+
+  constructor(groupService: GroupService, settings: SettingsService) {
+    super()
+    groupService
+      .listAll()
+      .pipe(first())
+      .subscribe((result) => (this.groups = result.results))
+  }
+
+  ngOnInit(): void {
+    if (this.type == 'view') {
+      this.title = $localize`Groups can view`
+    } else if (this.type == 'change') {
+      this.title = $localize`Groups can edit`
+      this.hint = $localize`Edit permissions also grant viewing permissions`
+    }
+
+    super.ngOnInit()
+  }
+}
diff --git a/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.scss b/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.scss
new file mode 100644 (file)
index 0000000..e69de29
similarity index 63%
rename from src-ui/src/app/components/common/input/share-user/share-user.component.ts
rename to src-ui/src/app/components/common/input/permissions-user/permissions-user.component.ts
index d132b3964289a389d19ed9cb8b5bf5c00a721c52..87ce08f086b3c5e2425c0de43908b3e653e2c29c 100644 (file)
@@ -3,21 +3,22 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'
 import { first } from 'rxjs/operators'
 import { PaperlessUser } from 'src/app/data/paperless-user'
 import { UserService } from 'src/app/services/rest/user.service'
+import { SettingsService } from 'src/app/services/settings.service'
 import { AbstractInputComponent } from '../abstract-input'
 
 @Component({
   providers: [
     {
       provide: NG_VALUE_ACCESSOR,
-      useExisting: forwardRef(() => ShareUserComponent),
+      useExisting: forwardRef(() => PermissionsUserComponent),
       multi: true,
     },
   ],
-  selector: 'app-share-user',
-  templateUrl: './share-user.component.html',
-  styleUrls: ['./share-user.component.scss'],
+  selector: 'app-permissions-user',
+  templateUrl: './permissions-user.component.html',
+  styleUrls: ['./permissions-user.component.scss'],
 })
-export class ShareUserComponent
+export class PermissionsUserComponent
   extends AbstractInputComponent<PaperlessUser>
   implements OnInit
 {
@@ -26,12 +27,17 @@ export class ShareUserComponent
   @Input()
   type: string
 
-  constructor(userService: UserService) {
+  constructor(userService: UserService, settings: SettingsService) {
     super()
     userService
       .listAll()
       .pipe(first())
-      .subscribe((result) => (this.users = result.results))
+      .subscribe(
+        (result) =>
+          (this.users = result.results.filter(
+            (u) => u.id !== settings.currentUser.id
+          ))
+      )
   }
 
   ngOnInit(): void {
index 56ba619bb6772ca45edefc71daaa6a9bc60daaee..673de6cb67e29a42f9f76cf73561ae23aaa85f08 100644 (file)
@@ -156,18 +156,18 @@ export class PermissionsSelectComponent
     if (this._inheritedPermissions.length == 0) return false
     else if (actionKey) {
       return this._inheritedPermissions.includes(
-        this.permissionsService.getPermissionCode({
-          action: PermissionAction[actionKey],
-          type: PermissionType[typeKey],
-        })
+        this.permissionsService.getPermissionCode(
+          PermissionAction[actionKey],
+          PermissionType[typeKey]
+        )
       )
     } else {
       return Object.values(PermissionAction).every((action) => {
         return this._inheritedPermissions.includes(
-          this.permissionsService.getPermissionCode({
-            action: action as PermissionAction,
-            type: PermissionType[typeKey],
-          })
+          this.permissionsService.getPermissionCode(
+            action as PermissionAction,
+            PermissionType[typeKey]
+          )
         )
       })
     }
index 6b729ee4c944e0afda05b8c5b7b5fd5de4b9ed02..d2100b59cfe6f6423f1773e7ef2d9ead8e021db7 100644 (file)
@@ -5,7 +5,7 @@
       <div class="input-group-text" i18n>of {{previewNumPages}}</div>
     </div>
 
-    <button *ifOwner="document?.owner" type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()">
+    <button *ifOwner="document" type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()">
         <svg class="buttonicon" fill="currentColor">
             <use xlink:href="assets/bootstrap-icons.svg#trash" />
         </svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
@@ -28,7 +28,7 @@
 
     </div>
 
-    <button *ifOwner="document?.owner" type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()">
+    <button *ifOwner="document" type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()">
         <svg class="buttonicon" fill="currentColor">
             <use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
         </svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span>
                     </ng-template>
                 </li>
 
-                <li [ngbNavItem]="6" *ifOwner="document?.owner">
+                <li [ngbNavItem]="6" *ifOwner="document">
                     <a ngbNavLink i18n>Permissions</a>
                     <ng-template ngbNavContent>
                         <div formGroupName="set_permissions">
-                            <app-share-user type="view" formControlName="view"></app-share-user>
-                            <app-share-user type="change" formControlName="change"></app-share-user>
+                            <h6 i18n>View</h6>
+                            <div formGroupName="view">
+                                <app-permissions-user type="view" formControlName="users"></app-permissions-user>
+                                <app-permissions-group type="view" formControlName="groups"></app-permissions-group>
+                            </div>
+                            <h6 i18n>Edit</h6>
+                            <div formGroupName="change">
+                                <app-permissions-user type="change" formControlName="users"></app-permissions-user>
+                                <app-permissions-group type="change" formControlName="groups"></app-permissions-group>
+                            </div>
                         </div>
                     </ng-template>
                 </li>
 
             <div [ngbNavOutlet]="nav" class="mt-2"></div>
 
-            <ng-container action="PermissionAction.Change" *ifObjectPermissions="document">
+            <ng-container *ifObjectPermissions="{ object: document, action: PermissionAction.Change }">
                 <button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || !(isDirty$ | async)">Discard</button>&nbsp;
                 <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save & next</button>&nbsp;
                 <button type="submit" class="btn btn-primary" *ifPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>&nbsp;
index 8d564f4b537249dcbbea15de536484b49d6e8df4..740ef46f9cae3054396a7500f232d17a645135ff 100644 (file)
@@ -85,8 +85,14 @@ export class DocumentDetailComponent
     archive_serial_number: new FormControl(),
     tags: new FormControl([]),
     set_permissions: new FormGroup({
-      view: new FormControl(null),
-      change: new FormControl(null),
+      view: new FormGroup({
+        users: new FormControl(null),
+        groups: new FormControl(null),
+      }),
+      change: new FormGroup({
+        users: new FormControl(null),
+        groups: new FormControl(null),
+      }),
     }),
   })
 
@@ -235,14 +241,7 @@ 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]),
-            },
+            set_permissions: doc.permissions,
           })
 
           this.isDirty$ = dirtyCheck(
@@ -297,14 +296,7 @@ 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]),
-    }
+    doc['set_permissions'] = doc.permissions
     this.documentForm.patchValue(doc)
     if (!this.userCanEdit) this.documentForm.disable()
   }
@@ -586,10 +578,10 @@ export class DocumentDetailComponent
   get commentsEnabled(): boolean {
     return (
       this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) &&
-      this.permissionsService.currentUserCan({
-        action: PermissionAction.View,
-        type: PermissionType.Document,
-      })
+      this.permissionsService.currentUserCan(
+        PermissionAction.View,
+        PermissionType.Document
+      )
     )
   }
 
index fd63a44b6b20c74d851f6edc7b98eef1fc383d66..7695c4abb35d3d0a216c32bfb7fab9e6c214fb92 100644 (file)
@@ -222,9 +222,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
   }
 
   userCanDelete(object: ObjectWithPermissions): boolean {
-    return (
-      !object.owner || this.permissionsService.currentUserIsOwner(object.owner)
-    )
+    return this.permissionsService.currentUserOwnsObject(object)
   }
 
   userCanEdit(object: ObjectWithPermissions): boolean {
index a95ada157dc6c63657d6cfc7c3f4ac654d7619c5..dce0cc02ab8dcc7c749397fdf31c5c202e6a257c 100644 (file)
@@ -1,8 +1,19 @@
 import { ObjectWithId } from './object-with-id'
 import { PaperlessUser } from './paperless-user'
 
+export interface PermissionsObject {
+  view: {
+    users: Array<number>
+    groups: Array<number>
+  }
+  change: {
+    users: Array<number>
+    groups: Array<number>
+  }
+}
+
 export interface ObjectWithPermissions extends ObjectWithId {
   owner?: PaperlessUser
 
-  permissions?: Array<[number, string]>
+  permissions?: PermissionsObject
 }
index c897b848d10e2f1d3246930bd96cb461b5f35a94..ccc90b70bec7042385efaca5fdd4c77c3147cfaf 100644 (file)
@@ -1,5 +1,6 @@
 import {
   Directive,
+  EmbeddedViewRef,
   Input,
   OnChanges,
   OnInit,
@@ -18,10 +19,12 @@ import {
 export class IfObjectPermissionsDirective implements OnInit, OnChanges {
   // The role the user must have
   @Input()
-  ifObjectPermissions: ObjectWithPermissions
+  ifObjectPermissions: {
+    object: ObjectWithPermissions
+    action: PermissionAction
+  }
 
-  @Input()
-  action: PermissionAction
+  createdView: EmbeddedViewRef<any>
 
   /**
    * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef
@@ -36,13 +39,16 @@ export class IfObjectPermissionsDirective implements OnInit, OnChanges {
 
   public ngOnInit(): void {
     if (
-      !this.ifObjectPermissions ||
+      !this.ifObjectPermissions?.object ||
       this.permissionsService.currentUserHasObjectPermissions(
-        this.action,
-        this.ifObjectPermissions
+        this.ifObjectPermissions.action,
+        this.ifObjectPermissions.object
       )
     ) {
-      this.viewContainerRef.createEmbeddedView(this.templateRef)
+      if (!this.createdView)
+        this.createdView = this.viewContainerRef.createEmbeddedView(
+          this.templateRef
+        )
     } else {
       this.viewContainerRef.clear()
     }
index 86fcd34579816147dbecb06a5b7bcac13aa96a60..082cc1679ba37e59402c6e94350904c7f88b8db6 100644 (file)
@@ -1,12 +1,13 @@
 import {
   Directive,
+  EmbeddedViewRef,
   Input,
   OnChanges,
   OnInit,
   TemplateRef,
   ViewContainerRef,
 } from '@angular/core'
-import { PaperlessUser } from '../data/paperless-user'
+import { ObjectWithPermissions } from '../data/object-with-permissions'
 import { PermissionsService } from '../services/permissions.service'
 
 @Directive({
@@ -15,7 +16,9 @@ import { PermissionsService } from '../services/permissions.service'
 export class IfOwnerDirective implements OnInit, OnChanges {
   // The role the user must have
   @Input()
-  ifOwner: PaperlessUser
+  ifOwner: ObjectWithPermissions
+
+  createdView: EmbeddedViewRef<any>
 
   /**
    * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef
@@ -29,11 +32,11 @@ export class IfOwnerDirective implements OnInit, OnChanges {
   ) {}
 
   public ngOnInit(): void {
-    if (
-      !this.ifOwner ||
-      this.permissionsService.currentUserIsOwner(this.ifOwner)
-    ) {
-      this.viewContainerRef.createEmbeddedView(this.templateRef)
+    if (this.permissionsService.currentUserOwnsObject(this.ifOwner)) {
+      if (!this.createdView)
+        this.createdView = this.viewContainerRef.createEmbeddedView(
+          this.templateRef
+        )
     } else {
       this.viewContainerRef.clear()
     }
index da93b65fcb287e9bb17db697815e1207fb7b9670..6dedaee2513954e889b7a167bf505fbcd5cfa923 100644 (file)
@@ -6,17 +6,19 @@ import {
   TemplateRef,
 } from '@angular/core'
 import {
-  PaperlessPermission,
+  PermissionAction,
   PermissionsService,
+  PermissionType,
 } from '../services/permissions.service'
 
 @Directive({
   selector: '[ifPermissions]',
 })
 export class IfPermissionsDirective implements OnInit {
-  // The role the user must have
   @Input()
-  ifPermissions: Array<PaperlessPermission> | PaperlessPermission
+  ifPermissions:
+    | Array<{ action: PermissionAction; type: PermissionType }>
+    | { action: PermissionAction; type: PermissionType }
 
   /**
    * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef
@@ -33,8 +35,8 @@ export class IfPermissionsDirective implements OnInit {
     if (
       []
         .concat(this.ifPermissions)
-        .every((perm: PaperlessPermission) =>
-          this.permissionsService.currentUserCan(perm)
+        .every((perm: { action: PermissionAction; type: PermissionType }) =>
+          this.permissionsService.currentUserCan(perm.action, perm.type)
         )
     ) {
       this.viewContainerRef.createEmbeddedView(this.templateRef)
index 39536ed559553adefebbf1335f9195afb195e51f..916408fe266394498888d5b07b836dc48857e743 100644 (file)
@@ -22,7 +22,10 @@ export class PermissionsGuard implements CanActivate {
     state: RouterStateSnapshot
   ): boolean | UrlTree {
     if (
-      !this.permissionsService.currentUserCan(route.data.requiredPermission)
+      !this.permissionsService.currentUserCan(
+        route.data.requiredPermission.action,
+        route.data.requiredPermission.type
+      )
     ) {
       this.toastService.showError(
         $localize`You don't have permissions to do that`
index 0f7edee220c06433e242e74adc10a44ad18a2790..b9bad0d96625697cc313ce4b94860b798de84868 100644 (file)
@@ -25,11 +25,6 @@ export enum PermissionType {
   Admin = '%s_logentry',
 }
 
-export interface PaperlessPermission {
-  action: PermissionAction
-  type: PermissionType
-}
-
 @Injectable({
   providedIn: 'root',
 })
@@ -42,25 +37,34 @@ export class PermissionsService {
     this.currentUser = currentUser
   }
 
-  public currentUserCan(permission: PaperlessPermission): boolean {
-    return this.permissions.includes(this.getPermissionCode(permission))
+  public currentUserCan(
+    action: PermissionAction,
+    type: PermissionType
+  ): boolean {
+    return this.permissions.includes(this.getPermissionCode(action, type))
   }
 
-  public currentUserIsOwner(owner: PaperlessUser): boolean {
-    return owner?.id === this.currentUser.id
+  public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
+    return !object || !object.owner || object.owner.id === this.currentUser.id
   }
 
   public currentUserHasObjectPermissions(
     action: string,
     object: ObjectWithPermissions
   ): boolean {
-    return (object.permissions[action] as Array<number>)?.includes(
-      this.currentUser.id
+    return (
+      this.currentUserOwnsObject(object) ||
+      (object.permissions[action]['users'] as Array<number>)?.includes(
+        this.currentUser.id
+      )
     )
   }
 
-  public getPermissionCode(permission: PaperlessPermission): string {
-    return permission.type.replace('%s', permission.action)
+  public getPermissionCode(
+    action: PermissionAction,
+    type: PermissionType
+  ): string {
+    return type.replace('%s', action)
   }
 
   public getPermissionKeys(permissionStr: string): {
index 43f3a758e3630e374b3df976688c10fea1a69b57..9ffa29c21d6be3392fc04cab097700d7bb47be76 100644 (file)
@@ -28,7 +28,7 @@ from .models import UiSettings
 from .models import PaperlessTask
 from .parsers import is_mime_type_supported
 
-from guardian.models import UserObjectPermission
+from guardian.models import GroupObjectPermission
 from guardian.shortcuts import assign_perm
 from guardian.shortcuts import remove_perm
 from guardian.shortcuts import get_users_with_perms
@@ -36,6 +36,8 @@ from guardian.shortcuts import get_users_with_perms
 from django.contrib.contenttypes.models import ContentType
 
 from django.contrib.auth.models import User
+from django.contrib.auth.models import Group
+from django.contrib.auth.models import Permission
 
 
 # https://www.django-rest-framework.org/api-guide/serializers/#example
@@ -83,14 +85,46 @@ class MatchingModelSerializer(serializers.ModelSerializer):
         return match
 
 
+def get_groups_with_only_permission(obj, codename):
+    ctype = ContentType.objects.get_for_model(obj)
+    permission = Permission.objects.get(content_type=ctype, codename=codename)
+    group_object_perm_group_ids = (
+        GroupObjectPermission.objects.filter(
+            object_pk=obj.pk,
+            content_type=ctype,
+        )
+        .filter(permission=permission)
+        .values_list("group_id")
+    )
+    return Group.objects.filter(id__in=group_object_perm_group_ids).distinct()
+
+
 class OwnedObjectSerializer(serializers.ModelSerializer):
     def get_permissions(self, obj):
-        content_type = ContentType.objects.get_for_model(obj)
-        user_object_perms = UserObjectPermission.objects.filter(
-            object_pk=obj.pk,
-            content_type=content_type,
-        ).values_list("user", "permission__codename")
-        return list(user_object_perms)
+        view_codename = f"view_{obj.__class__.__name__.lower()}"
+        change_codename = f"change_{obj.__class__.__name__.lower()}"
+        return {
+            "view": {
+                "users": get_users_with_perms(
+                    obj,
+                    only_with_perms_in=[view_codename],
+                ).values_list("id", flat=True),
+                "groups": get_groups_with_only_permission(
+                    obj,
+                    codename=view_codename,
+                ).values_list("id", flat=True),
+            },
+            "change": {
+                "users": get_users_with_perms(
+                    obj,
+                    only_with_perms_in=[change_codename],
+                ).values_list("id", flat=True),
+                "groups": get_groups_with_only_permission(
+                    obj,
+                    codename=change_codename,
+                ).values_list("id", flat=True),
+            },
+        }
 
     permissions = SerializerMethodField(read_only=True)
 
@@ -111,19 +145,34 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
                 )
         return users
 
+    def _validate_group_ids(self, group_ids):
+        groups = Group.objects.none()
+        if group_ids is not None:
+            groups = Group.objects.filter(id__in=group_ids)
+            if not groups.count() == len(group_ids):
+                raise serializers.ValidationError(
+                    "Some groups in don't exist or were specified twice.",
+                )
+        return groups
+
     def validate_set_permissions(self, set_permissions):
-        user_dict = {
-            "view": User.objects.none(),
-            "change": User.objects.none(),
+        permissions_dict = {
+            "view": {
+                "users": User.objects.none(),
+                "groups": Group.objects.none(),
+            },
+            "change": {
+                "users": User.objects.none(),
+                "groups": Group.objects.none(),
+            },
         }
         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 set_permissions:
-                change_list = set_permissions["change"]
-                user_dict["change"] = self._validate_user_ids(change_list)
-        return user_dict
+            for action in permissions_dict:
+                users = set_permissions[action]["users"]
+                permissions_dict[action]["users"] = self._validate_user_ids(users)
+                groups = set_permissions[action]["groups"]
+                permissions_dict[action]["groups"] = self._validate_group_ids(groups)
+        return permissions_dict
 
     def __init__(self, *args, **kwargs):
         self.user = kwargs.pop("user", None)
@@ -132,7 +181,8 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
     def _set_permissions(self, permissions, object):
         for action in permissions:
             permission = f"{action}_{object.__class__.__name__.lower()}"
-            users_to_add = permissions[action]
+            # users
+            users_to_add = permissions[action]["users"]
             users_to_remove = get_users_with_perms(
                 object,
                 only_with_perms_in=[permission],
@@ -148,6 +198,23 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
                         user,
                         object,
                     )
+            # groups
+            groups_to_add = permissions[action]["groups"]
+            groups_to_remove = get_groups_with_only_permission(
+                object,
+                permission,
+            ).difference(groups_to_add)
+            for group in groups_to_remove:
+                remove_perm(permission, group, object)
+            for group in groups_to_add:
+                assign_perm(permission, group, object)
+                if action == "change":
+                    # change gives view too
+                    assign_perm(
+                        f"view_{object.__class__.__name__.lower()}",
+                        group,
+                        object,
+                    )
 
     def create(self, validated_data):
         if self.user and (
index e161edfac69e74762c70e1f4460db24097bbd101..5cfc13672f74acf260c6516985b64b077c58963b 100644 (file)
@@ -158,7 +158,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
         response = self.client.get("/api/documents/?fields=", format="json")
         self.assertEqual(response.status_code, 200)
         results = response.data["results"]
-        self.assertEqual(results_full, results)
+        self.assertEqual(len(results_full[0]), len(results[0]))
 
         response = self.client.get("/api/documents/?fields=dgfhs", format="json")
         self.assertEqual(response.status_code, 200)