-
diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts
index 19c9b4af0..203ada53f 100644
--- a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts
+++ b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts
@@ -6,10 +6,12 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { OcrTemplate } from 'src/app/data/ocr-template'
+import { User } from 'src/app/data/user'
import { DirtyFormGuard } from 'src/app/guards/dirty-form.guard'
import { PermissionsService } from 'src/app/services/permissions.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service'
+import { UserService } from 'src/app/services/rest/user.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { OcrTemplateEditorComponent } from './ocr-template-editor/ocr-template-editor.component'
@@ -40,19 +42,24 @@ describe('OcrTemplatesComponent', () => {
TestBed.configureTestingModule({
imports: [NgxBootstrapIconsModule.pick(allIcons), OcrTemplatesComponent],
providers: [
- {
- provide: PermissionsService,
- useValue: {
- currentUserCan: () => true,
- currentUserHasObjectPermissions: () => true,
- currentUserOwnsObject: () => true,
- },
- },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
})
+ TestBed.inject(PermissionsService).initialize(
+ [
+ 'view_ocrtemplate',
+ 'add_ocrtemplate',
+ 'change_ocrtemplate',
+ 'delete_ocrtemplate',
+ ],
+ { id: 1, groups: [], is_superuser: false } as User
+ )
+ jest
+ .spyOn(TestBed.inject(UserService), 'listAll')
+ .mockReturnValue(of({ count: 0, all: [], results: [] }))
+
ocrTemplateService = TestBed.inject(OcrTemplateService)
jest
.spyOn(ocrTemplateService, 'listAll')
@@ -78,6 +85,107 @@ describe('OcrTemplatesComponent', () => {
expect(text).toContain('Invoice')
})
+ it.each([
+ {
+ name: 'owned',
+ owner: 1,
+ user_can_change: true,
+ editable: true,
+ deletable: true,
+ },
+ {
+ name: 'unowned',
+ owner: null,
+ user_can_change: true,
+ editable: true,
+ deletable: true,
+ },
+ {
+ name: 'view-only shared',
+ owner: 2,
+ user_can_change: false,
+ editable: false,
+ deletable: false,
+ },
+ {
+ name: 'editable shared',
+ owner: 2,
+ user_can_change: true,
+ editable: true,
+ deletable: false,
+ },
+ ])(
+ 'should respect permissions for $name templates',
+ async ({ owner, user_can_change, editable, deletable }) => {
+ const template = { ...templates[0], owner, user_can_change }
+ component.templates.set([template])
+ fixture.detectChanges()
+ await fixture.whenStable()
+
+ expect(fixture.nativeElement.querySelector('.text-start').disabled).toBe(
+ !editable
+ )
+ expect(
+ fixture.nativeElement.querySelector('.form-switch input').disabled
+ ).toBe(!editable)
+ const buttons = Array.from
(
+ fixture.nativeElement.querySelectorAll('button')
+ )
+ for (const button of buttons.filter(
+ (button) => button.textContent.trim() === 'Edit'
+ )) {
+ expect(button.disabled).toBe(!editable)
+ }
+ for (const button of buttons.filter(
+ (button) => button.textContent.trim() === 'Delete'
+ )) {
+ expect(button.disabled).toBe(!deletable)
+ }
+ }
+ )
+
+ it('should allow a superuser to manage another users template', () => {
+ TestBed.inject(PermissionsService).initialize([], {
+ id: 1,
+ is_superuser: true,
+ } as User)
+ component.templates.set([
+ { ...templates[0], owner: 2, user_can_change: false },
+ ])
+ fixture.detectChanges()
+ expect(fixture.nativeElement.querySelector('.text-start').disabled).toBe(
+ false
+ )
+ expect(
+ fixture.nativeElement.querySelector('.btn-outline-danger').disabled
+ ).toBe(false)
+ })
+
+ it('should not open mutation dialogs or patch a view-only template', () => {
+ const template = { ...templates[0], owner: 2, user_can_change: false }
+ const open = jest.spyOn(modalService, 'open')
+ const patch = jest.spyOn(ocrTemplateService, 'patch')
+ component.editTemplate(template)
+ component.toggleTemplateEnabled(template)
+ component.deleteTemplate(template)
+ expect(open).not.toHaveBeenCalled()
+ expect(patch).not.toHaveBeenCalled()
+ })
+
+ it('should require global edit permission even for an owned template', () => {
+ TestBed.inject(PermissionsService).initialize(['view_ocrtemplate'], {
+ id: 1,
+ } as User)
+ fixture.destroy()
+ fixture = TestBed.createComponent(OcrTemplatesComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ expect(fixture.nativeElement.querySelector('.text-start').disabled).toBe(
+ true
+ )
+ expect(fixture.nativeElement.querySelector('.form-switch input')).toBeNull()
+ })
+
it('should support create, show notification on error / success', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, 'showSuccess')
diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts
index 4daaf1d31..06c646b93 100644
--- a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts
+++ b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts
@@ -7,7 +7,11 @@ import { DocumentType } from 'src/app/data/document-type'
import { OcrTemplate } from 'src/app/data/ocr-template'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { DirtyFormGuard } from 'src/app/guards/dirty-form.guard'
-import { PermissionsService } from 'src/app/services/permissions.service'
+import {
+ PermissionAction,
+ PermissionType,
+ PermissionsService,
+} from 'src/app/services/permissions.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service'
import { ToastService } from 'src/app/services/toast.service'
@@ -71,6 +75,7 @@ export class OcrTemplatesComponent
}
editTemplate(template?: OcrTemplate) {
+ if (template && !this.userCanEdit(template)) return
const modal = this.modalService.open(OcrTemplateEditorComponent, {
backdrop: 'static',
keyboard: false,
@@ -105,6 +110,7 @@ export class OcrTemplatesComponent
}
toggleTemplateEnabled(template: OcrTemplate) {
+ if (!this.userCanEdit(template)) return
this.ocrTemplateService
.patch({ id: template.id, enabled: template.enabled } as OcrTemplate)
.subscribe({
@@ -127,6 +133,14 @@ export class OcrTemplatesComponent
}
deleteTemplate(template: OcrTemplate) {
+ if (
+ !this.permissionsService.currentUserCan(
+ PermissionAction.Delete,
+ PermissionType.OcrTemplate
+ ) ||
+ !this.permissionsService.currentUserOwnsObject(template)
+ )
+ return
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
@@ -156,4 +170,17 @@ export class OcrTemplatesComponent
})
})
}
+
+ userCanEdit(template: OcrTemplate): boolean {
+ return (
+ this.permissionsService.currentUserCan(
+ PermissionAction.Change,
+ PermissionType.OcrTemplate
+ ) &&
+ this.permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.Change,
+ template
+ )
+ )
+ }
}
diff --git a/src-ui/src/app/data/ocr-template.ts b/src-ui/src/app/data/ocr-template.ts
index 453647f3e..5baaafa5d 100644
--- a/src-ui/src/app/data/ocr-template.ts
+++ b/src-ui/src/app/data/ocr-template.ts
@@ -1,5 +1,5 @@
import { CustomFieldDataType } from './custom-field'
-import { ObjectWithId } from './object-with-id'
+import { ObjectWithPermissions } from './object-with-permissions'
export enum OcrZoneTarget {
CustomField = 'custom_field',
@@ -64,7 +64,7 @@ export interface OcrTemplateZone {
zone_source_height?: number
}
-export interface OcrTemplate extends ObjectWithId {
+export interface OcrTemplate extends ObjectWithPermissions {
name: string
document_type: number
sample_document: number | null