mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Refactor permissions API endpoints, UI group permissions
This commit is contained in:
		| @@ -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 | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|   ], | ||||
|   | ||||
| @@ -11,11 +11,19 @@ | ||||
|     <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> | ||||
|  | ||||
|   | ||||
| @@ -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), | ||||
|         }), | ||||
|       }), | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -11,11 +11,19 @@ | ||||
|       <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> | ||||
|  | ||||
|   | ||||
| @@ -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), | ||||
|         }), | ||||
|       }), | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
|   | ||||
| @@ -16,11 +16,19 @@ | ||||
|     <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> | ||||
|  | ||||
|   | ||||
| @@ -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), | ||||
|         }), | ||||
|       }), | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -14,11 +14,19 @@ | ||||
|       <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> | ||||
|  | ||||
|   | ||||
| @@ -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), | ||||
|         }), | ||||
|       }), | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -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> | ||||
| @@ -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() | ||||
|   } | ||||
| } | ||||
| @@ -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 { | ||||
| @@ -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] | ||||
|           ) | ||||
|         ) | ||||
|       }) | ||||
|     } | ||||
|   | ||||
| @@ -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> | ||||
| @@ -178,12 +178,20 @@ | ||||
|                     </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> | ||||
| @@ -191,7 +199,7 @@ | ||||
|  | ||||
|             <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>  | ||||
|                 <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save & next</button>  | ||||
|                 <button type="submit" class="btn btn-primary" *ifPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>  | ||||
|   | ||||
| @@ -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 | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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` | ||||
|   | ||||
| @@ -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): { | ||||
|   | ||||
| @@ -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 ( | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon