Refactor permissions API endpoints, UI group permissions

This commit is contained in:
Michael Shamoon 2022-12-07 21:11:47 -08:00
parent f2d635671d
commit 211fbf0cf6
29 changed files with 353 additions and 139 deletions

View File

@ -81,10 +81,10 @@ export class AppComponent implements OnInit, OnDestroy {
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS) this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
) { ) {
if ( if (
this.permissionsService.currentUserCan({ this.permissionsService.currentUserCan(
action: PermissionAction.View, PermissionAction.View,
type: PermissionType.Document, PermissionType.Document
}) )
) { ) {
this.toastService.show({ this.toastService.show({
title: $localize`Document added`, title: $localize`Document added`,
@ -246,10 +246,10 @@ export class AppComponent implements OnInit, OnDestroy {
public get dragDropEnabled(): boolean { public get dragDropEnabled(): boolean {
return ( return (
!this.router.url.includes('dashboard') && !this.router.url.includes('dashboard') &&
this.permissionsService.currentUserCan({ this.permissionsService.currentUserCan(
action: PermissionAction.Add, PermissionAction.Add,
type: PermissionType.Document, PermissionType.Document
}) )
) )
} }

View File

@ -84,6 +84,10 @@ import { GroupEditDialogComponent } from './components/common/edit-dialog/group-
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component' 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 { 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 { 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 localeBe from '@angular/common/locales/be'
import localeCs from '@angular/common/locales/cs' 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 localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr' import localeTr from '@angular/common/locales/tr'
import localeZh from '@angular/common/locales/zh' 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(localeBe)
registerLocaleData(localeCs) registerLocaleData(localeCs)
@ -198,7 +199,8 @@ function initializeApp(settings: SettingsService) {
PermissionsSelectComponent, PermissionsSelectComponent,
MailAccountEditDialogComponent, MailAccountEditDialogComponent,
MailRuleEditDialogComponent, MailRuleEditDialogComponent,
ShareUserComponent, PermissionsUserComponent,
PermissionsGroupComponent,
IfOwnerDirective, IfOwnerDirective,
IfObjectPermissionsDirective, IfObjectPermissionsDirective,
], ],

View File

@ -11,11 +11,19 @@
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <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> <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> <h5 i18n>Permissions</h5>
<div formGroupName="set_permissions"> <div formGroupName="set_permissions">
<app-share-user type="view" formControlName="view"></app-share-user> <h6 i18n>View</h6>
<app-share-user type="change" formControlName="change"></app-share-user> <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>
</div> </div>

View File

@ -31,8 +31,14 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
match: new FormControl(''), match: new FormControl(''),
is_insensitive: new FormControl(true), is_insensitive: new FormControl(true),
set_permissions: new FormGroup({ set_permissions: new FormGroup({
view: new FormControl(null), view: new FormGroup({
change: new FormControl(null), users: new FormControl(null),
groups: new FormControl(null),
}),
change: new FormGroup({
users: new FormControl(null),
groups: new FormControl(null),
}),
}), }),
}) })
} }

View File

@ -11,11 +11,19 @@
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <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> <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> <h5 i18n>Permissions</h5>
<div formGroupName="set_permissions"> <div formGroupName="set_permissions">
<app-share-user type="view" formControlName="view"></app-share-user> <h6 i18n>View</h6>
<app-share-user type="change" formControlName="change"></app-share-user> <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>
</div> </div>

View File

@ -31,8 +31,14 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
match: new FormControl(''), match: new FormControl(''),
is_insensitive: new FormControl(true), is_insensitive: new FormControl(true),
set_permissions: new FormGroup({ set_permissions: new FormGroup({
view: new FormControl(null), view: new FormGroup({
change: new FormControl(null), users: new FormControl(null),
groups: new FormControl(null),
}),
change: new FormGroup({
users: new FormControl(null),
groups: new FormControl(null),
}),
}), }),
}) })
} }

View File

@ -39,14 +39,7 @@ export abstract class EditDialogComponent<
ngOnInit(): void { ngOnInit(): void {
if (this.object != null) { if (this.object != null) {
if (this.object['permissions']) { if (this.object['permissions']) {
this.object['set_permissions'] = { this.object['set_permissions'] = this.object['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.objectForm.patchValue(this.object) this.objectForm.patchValue(this.object)
} }

View File

@ -16,11 +16,19 @@
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <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> <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> <h5 i18n>Permissions</h5>
<div formGroupName="set_permissions"> <div formGroupName="set_permissions">
<app-share-user type="view" formControlName="view"></app-share-user> <h6 i18n>View</h6>
<app-share-user type="change" formControlName="change"></app-share-user> <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>
</div> </div>

View File

@ -42,8 +42,14 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
match: new FormControl(''), match: new FormControl(''),
is_insensitive: new FormControl(true), is_insensitive: new FormControl(true),
set_permissions: new FormGroup({ set_permissions: new FormGroup({
view: new FormControl(null), view: new FormGroup({
change: new FormControl(null), users: new FormControl(null),
groups: new FormControl(null),
}),
change: new FormGroup({
users: new FormControl(null),
groups: new FormControl(null),
}),
}), }),
}) })
} }

View File

@ -14,11 +14,19 @@
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <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> <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> <h5 i18n>Permissions</h5>
<div formGroupName="set_permissions"> <div formGroupName="set_permissions">
<app-share-user type="view" formControlName="view"></app-share-user> <h6 i18n>View</h6>
<app-share-user type="change" formControlName="change"></app-share-user> <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>
</div> </div>

View File

@ -34,8 +34,14 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
match: new FormControl(''), match: new FormControl(''),
is_insensitive: new FormControl(true), is_insensitive: new FormControl(true),
set_permissions: new FormGroup({ set_permissions: new FormGroup({
view: new FormControl(null), view: new FormGroup({
change: new FormControl(null), users: new FormControl(null),
groups: new FormControl(null),
}),
change: new FormGroup({
users: new FormControl(null),
groups: new FormControl(null),
}),
}), }),
}) })
} }

View File

@ -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>

View File

@ -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()
}
}

View File

@ -3,21 +3,22 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { first } from 'rxjs/operators' import { first } from 'rxjs/operators'
import { PaperlessUser } from 'src/app/data/paperless-user' import { PaperlessUser } from 'src/app/data/paperless-user'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { AbstractInputComponent } from '../abstract-input' import { AbstractInputComponent } from '../abstract-input'
@Component({ @Component({
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ShareUserComponent), useExisting: forwardRef(() => PermissionsUserComponent),
multi: true, multi: true,
}, },
], ],
selector: 'app-share-user', selector: 'app-permissions-user',
templateUrl: './share-user.component.html', templateUrl: './permissions-user.component.html',
styleUrls: ['./share-user.component.scss'], styleUrls: ['./permissions-user.component.scss'],
}) })
export class ShareUserComponent export class PermissionsUserComponent
extends AbstractInputComponent<PaperlessUser> extends AbstractInputComponent<PaperlessUser>
implements OnInit implements OnInit
{ {
@ -26,12 +27,17 @@ export class ShareUserComponent
@Input() @Input()
type: string type: string
constructor(userService: UserService) { constructor(userService: UserService, settings: SettingsService) {
super() super()
userService userService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.users = result.results)) .subscribe(
(result) =>
(this.users = result.results.filter(
(u) => u.id !== settings.currentUser.id
))
)
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -156,18 +156,18 @@ export class PermissionsSelectComponent
if (this._inheritedPermissions.length == 0) return false if (this._inheritedPermissions.length == 0) return false
else if (actionKey) { else if (actionKey) {
return this._inheritedPermissions.includes( return this._inheritedPermissions.includes(
this.permissionsService.getPermissionCode({ this.permissionsService.getPermissionCode(
action: PermissionAction[actionKey], PermissionAction[actionKey],
type: PermissionType[typeKey], PermissionType[typeKey]
}) )
) )
} else { } else {
return Object.values(PermissionAction).every((action) => { return Object.values(PermissionAction).every((action) => {
return this._inheritedPermissions.includes( return this._inheritedPermissions.includes(
this.permissionsService.getPermissionCode({ this.permissionsService.getPermissionCode(
action: action as PermissionAction, action as PermissionAction,
type: PermissionType[typeKey], PermissionType[typeKey]
}) )
) )
}) })
} }

View File

@ -5,7 +5,7 @@
<div class="input-group-text" i18n>of {{previewNumPages}}</div> <div class="input-group-text" i18n>of {{previewNumPages}}</div>
</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"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span> </svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
@ -28,7 +28,7 @@
</div> </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"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
</svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span> </svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span>
@ -178,12 +178,20 @@
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="6" *ifOwner="document?.owner"> <li [ngbNavItem]="6" *ifOwner="document">
<a ngbNavLink i18n>Permissions</a> <a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div formGroupName="set_permissions"> <div formGroupName="set_permissions">
<app-share-user type="view" formControlName="view"></app-share-user> <h6 i18n>View</h6>
<app-share-user type="change" formControlName="change"></app-share-user> <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>
</ng-template> </ng-template>
</li> </li>
@ -191,7 +199,7 @@
<div [ngbNavOutlet]="nav" class="mt-2"></div> <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-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="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; <button type="submit" class="btn btn-primary" *ifPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>&nbsp;

View File

@ -85,8 +85,14 @@ export class DocumentDetailComponent
archive_serial_number: new FormControl(), archive_serial_number: new FormControl(),
tags: new FormControl([]), tags: new FormControl([]),
set_permissions: new FormGroup({ set_permissions: new FormGroup({
view: new FormControl(null), view: new FormGroup({
change: new FormControl(null), 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, storage_path: doc.storage_path,
archive_serial_number: doc.archive_serial_number, archive_serial_number: doc.archive_serial_number,
tags: [...doc.tags], tags: [...doc.tags],
set_permissions: { set_permissions: doc.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( this.isDirty$ = dirtyCheck(
@ -297,14 +296,7 @@ export class DocumentDetailComponent
}, },
}) })
this.title = this.documentTitlePipe.transform(doc.title) this.title = this.documentTitlePipe.transform(doc.title)
doc['set_permissions'] = { doc['set_permissions'] = doc.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) this.documentForm.patchValue(doc)
if (!this.userCanEdit) this.documentForm.disable() if (!this.userCanEdit) this.documentForm.disable()
} }
@ -586,10 +578,10 @@ export class DocumentDetailComponent
get commentsEnabled(): boolean { get commentsEnabled(): boolean {
return ( return (
this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) && this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) &&
this.permissionsService.currentUserCan({ this.permissionsService.currentUserCan(
action: PermissionAction.View, PermissionAction.View,
type: PermissionType.Document, PermissionType.Document
}) )
) )
} }

View File

@ -222,9 +222,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
} }
userCanDelete(object: ObjectWithPermissions): boolean { userCanDelete(object: ObjectWithPermissions): boolean {
return ( return this.permissionsService.currentUserOwnsObject(object)
!object.owner || this.permissionsService.currentUserIsOwner(object.owner)
)
} }
userCanEdit(object: ObjectWithPermissions): boolean { userCanEdit(object: ObjectWithPermissions): boolean {

View File

@ -1,8 +1,19 @@
import { ObjectWithId } from './object-with-id' import { ObjectWithId } from './object-with-id'
import { PaperlessUser } from './paperless-user' 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 { export interface ObjectWithPermissions extends ObjectWithId {
owner?: PaperlessUser owner?: PaperlessUser
permissions?: Array<[number, string]> permissions?: PermissionsObject
} }

View File

@ -1,5 +1,6 @@
import { import {
Directive, Directive,
EmbeddedViewRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
@ -18,10 +19,12 @@ import {
export class IfObjectPermissionsDirective implements OnInit, OnChanges { export class IfObjectPermissionsDirective implements OnInit, OnChanges {
// The role the user must have // The role the user must have
@Input() @Input()
ifObjectPermissions: ObjectWithPermissions ifObjectPermissions: {
object: ObjectWithPermissions
action: PermissionAction
}
@Input() createdView: EmbeddedViewRef<any>
action: PermissionAction
/** /**
* @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef * @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 { public ngOnInit(): void {
if ( if (
!this.ifObjectPermissions || !this.ifObjectPermissions?.object ||
this.permissionsService.currentUserHasObjectPermissions( this.permissionsService.currentUserHasObjectPermissions(
this.action, this.ifObjectPermissions.action,
this.ifObjectPermissions this.ifObjectPermissions.object
) )
) { ) {
this.viewContainerRef.createEmbeddedView(this.templateRef) if (!this.createdView)
this.createdView = this.viewContainerRef.createEmbeddedView(
this.templateRef
)
} else { } else {
this.viewContainerRef.clear() this.viewContainerRef.clear()
} }

View File

@ -1,12 +1,13 @@
import { import {
Directive, Directive,
EmbeddedViewRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
TemplateRef, TemplateRef,
ViewContainerRef, ViewContainerRef,
} from '@angular/core' } from '@angular/core'
import { PaperlessUser } from '../data/paperless-user' import { ObjectWithPermissions } from '../data/object-with-permissions'
import { PermissionsService } from '../services/permissions.service' import { PermissionsService } from '../services/permissions.service'
@Directive({ @Directive({
@ -15,7 +16,9 @@ import { PermissionsService } from '../services/permissions.service'
export class IfOwnerDirective implements OnInit, OnChanges { export class IfOwnerDirective implements OnInit, OnChanges {
// The role the user must have // The role the user must have
@Input() @Input()
ifOwner: PaperlessUser ifOwner: ObjectWithPermissions
createdView: EmbeddedViewRef<any>
/** /**
* @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef * @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 { public ngOnInit(): void {
if ( if (this.permissionsService.currentUserOwnsObject(this.ifOwner)) {
!this.ifOwner || if (!this.createdView)
this.permissionsService.currentUserIsOwner(this.ifOwner) this.createdView = this.viewContainerRef.createEmbeddedView(
) { this.templateRef
this.viewContainerRef.createEmbeddedView(this.templateRef) )
} else { } else {
this.viewContainerRef.clear() this.viewContainerRef.clear()
} }

View File

@ -6,17 +6,19 @@ import {
TemplateRef, TemplateRef,
} from '@angular/core' } from '@angular/core'
import { import {
PaperlessPermission, PermissionAction,
PermissionsService, PermissionsService,
PermissionType,
} from '../services/permissions.service' } from '../services/permissions.service'
@Directive({ @Directive({
selector: '[ifPermissions]', selector: '[ifPermissions]',
}) })
export class IfPermissionsDirective implements OnInit { export class IfPermissionsDirective implements OnInit {
// The role the user must have
@Input() @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 * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef
@ -33,8 +35,8 @@ export class IfPermissionsDirective implements OnInit {
if ( if (
[] []
.concat(this.ifPermissions) .concat(this.ifPermissions)
.every((perm: PaperlessPermission) => .every((perm: { action: PermissionAction; type: PermissionType }) =>
this.permissionsService.currentUserCan(perm) this.permissionsService.currentUserCan(perm.action, perm.type)
) )
) { ) {
this.viewContainerRef.createEmbeddedView(this.templateRef) this.viewContainerRef.createEmbeddedView(this.templateRef)

View File

@ -22,7 +22,10 @@ export class PermissionsGuard implements CanActivate {
state: RouterStateSnapshot state: RouterStateSnapshot
): boolean | UrlTree { ): boolean | UrlTree {
if ( if (
!this.permissionsService.currentUserCan(route.data.requiredPermission) !this.permissionsService.currentUserCan(
route.data.requiredPermission.action,
route.data.requiredPermission.type
)
) { ) {
this.toastService.showError( this.toastService.showError(
$localize`You don't have permissions to do that` $localize`You don't have permissions to do that`

View File

@ -25,11 +25,6 @@ export enum PermissionType {
Admin = '%s_logentry', Admin = '%s_logentry',
} }
export interface PaperlessPermission {
action: PermissionAction
type: PermissionType
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -42,25 +37,34 @@ export class PermissionsService {
this.currentUser = currentUser this.currentUser = currentUser
} }
public currentUserCan(permission: PaperlessPermission): boolean { public currentUserCan(
return this.permissions.includes(this.getPermissionCode(permission)) action: PermissionAction,
type: PermissionType
): boolean {
return this.permissions.includes(this.getPermissionCode(action, type))
} }
public currentUserIsOwner(owner: PaperlessUser): boolean { public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
return owner?.id === this.currentUser.id return !object || !object.owner || object.owner.id === this.currentUser.id
} }
public currentUserHasObjectPermissions( public currentUserHasObjectPermissions(
action: string, action: string,
object: ObjectWithPermissions object: ObjectWithPermissions
): boolean { ): boolean {
return (object.permissions[action] as Array<number>)?.includes( return (
this.currentUser.id this.currentUserOwnsObject(object) ||
(object.permissions[action]['users'] as Array<number>)?.includes(
this.currentUser.id
)
) )
} }
public getPermissionCode(permission: PaperlessPermission): string { public getPermissionCode(
return permission.type.replace('%s', permission.action) action: PermissionAction,
type: PermissionType
): string {
return type.replace('%s', action)
} }
public getPermissionKeys(permissionStr: string): { public getPermissionKeys(permissionStr: string): {

View File

@ -28,7 +28,7 @@ from .models import UiSettings
from .models import PaperlessTask from .models import PaperlessTask
from .parsers import is_mime_type_supported 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 assign_perm
from guardian.shortcuts import remove_perm from guardian.shortcuts import remove_perm
from guardian.shortcuts import get_users_with_perms 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.contenttypes.models import ContentType
from django.contrib.auth.models import User 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 # https://www.django-rest-framework.org/api-guide/serializers/#example
@ -83,14 +85,46 @@ class MatchingModelSerializer(serializers.ModelSerializer):
return match 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): class OwnedObjectSerializer(serializers.ModelSerializer):
def get_permissions(self, obj): def get_permissions(self, obj):
content_type = ContentType.objects.get_for_model(obj) view_codename = f"view_{obj.__class__.__name__.lower()}"
user_object_perms = UserObjectPermission.objects.filter( change_codename = f"change_{obj.__class__.__name__.lower()}"
object_pk=obj.pk, return {
content_type=content_type, "view": {
).values_list("user", "permission__codename") "users": get_users_with_perms(
return list(user_object_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) permissions = SerializerMethodField(read_only=True)
@ -111,19 +145,34 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
) )
return users 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): def validate_set_permissions(self, set_permissions):
user_dict = { permissions_dict = {
"view": User.objects.none(), "view": {
"change": User.objects.none(), "users": User.objects.none(),
"groups": Group.objects.none(),
},
"change": {
"users": User.objects.none(),
"groups": Group.objects.none(),
},
} }
if set_permissions is not None: if set_permissions is not None:
if "view" in set_permissions: for action in permissions_dict:
view_list = set_permissions["view"] users = set_permissions[action]["users"]
user_dict["view"] = self._validate_user_ids(view_list) permissions_dict[action]["users"] = self._validate_user_ids(users)
if "change" in set_permissions: groups = set_permissions[action]["groups"]
change_list = set_permissions["change"] permissions_dict[action]["groups"] = self._validate_group_ids(groups)
user_dict["change"] = self._validate_user_ids(change_list) return permissions_dict
return user_dict
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None) self.user = kwargs.pop("user", None)
@ -132,7 +181,8 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
def _set_permissions(self, permissions, object): def _set_permissions(self, permissions, object):
for action in permissions: for action in permissions:
permission = f"{action}_{object.__class__.__name__.lower()}" 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( users_to_remove = get_users_with_perms(
object, object,
only_with_perms_in=[permission], only_with_perms_in=[permission],
@ -148,6 +198,23 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
user, user,
object, 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): def create(self, validated_data):
if self.user and ( if self.user and (

View File

@ -158,7 +158,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
response = self.client.get("/api/documents/?fields=", format="json") response = self.client.get("/api/documents/?fields=", format="json")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
results = response.data["results"] 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") response = self.client.get("/api/documents/?fields=dgfhs", format="json")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)