From 3624922a63c8f0bafc3b65b33ae9c6d1edae0831 Mon Sep 17 00:00:00 2001 From: menoude-aneo Date: Mon, 17 Nov 2025 13:59:49 +0100 Subject: [PATCH 1/2] feat: Add guard for download result actions --- .../components/inspection/byte-array.component.html | 2 +- .../components/inspection/byte-array.component.ts | 7 +++++++ .../table/cells/byte-array-cell.component.html | 2 +- .../table/cells/byte-array-cell.component.ts | 7 +++++++ src/app/results/components/table.component.ts | 12 ++++++++++++ src/app/results/show.component.html | 6 +++++- src/app/results/show.component.ts | 7 +++++++ 7 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/app/components/inspection/byte-array.component.html b/src/app/components/inspection/byte-array.component.html index e5df36d10..4c3442c85 100644 --- a/src/app/components/inspection/byte-array.component.html +++ b/src/app/components/inspection/byte-array.component.html @@ -7,7 +7,7 @@ } @else { - Binary } - @if (byteLength) { + @if (byteLength && hasDownloadPermission) { diff --git a/src/app/components/inspection/byte-array.component.ts b/src/app/components/inspection/byte-array.component.ts index dcec248c1..7b72fa121 100644 --- a/src/app/components/inspection/byte-array.component.ts +++ b/src/app/components/inspection/byte-array.component.ts @@ -8,6 +8,7 @@ import { PrettyPipe } from '@pipes/pretty.pipe'; import { ByteArrayService } from '@services/byte-array.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; /** * Displays a byte array in armonik inspection pages. @@ -50,6 +51,7 @@ export class ByteArrayComponent { private readonly iconsService = inject(IconsService); private readonly clipboard = inject(Clipboard); private readonly notificationService = inject(NotificationService); + private readonly userService = inject(UserService); /** * Returns the icon associated with that name. @@ -90,4 +92,9 @@ export class ByteArrayComponent { this.notificationService.success('Copied to clipboard'); } } + + get hasDownloadPermission(): boolean { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:DownloadResultData'); + } } \ No newline at end of file diff --git a/src/app/components/table/cells/byte-array-cell.component.html b/src/app/components/table/cells/byte-array-cell.component.html index ad76f6cb8..33b68a6fb 100644 --- a/src/app/components/table/cells/byte-array-cell.component.html +++ b/src/app/components/table/cells/byte-array-cell.component.html @@ -14,7 +14,7 @@ } - @if (decodedData || byteLength) { + @if ((decodedData || byteLength) && hasDownloadPermission) { diff --git a/src/app/components/table/cells/byte-array-cell.component.ts b/src/app/components/table/cells/byte-array-cell.component.ts index 9bff80d14..dc6495e7d 100644 --- a/src/app/components/table/cells/byte-array-cell.component.ts +++ b/src/app/components/table/cells/byte-array-cell.component.ts @@ -6,6 +6,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { ByteArrayService } from '@services/byte-array.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; /** * Displays a byte array in armonik tables. @@ -51,6 +52,7 @@ export class ByteArrayComponent { private readonly iconsService = inject(IconsService); readonly clipboard = inject(Clipboard); private readonly notificationService = inject(NotificationService); + private readonly userService = inject(UserService); /** * Download the byteArray in a binary file. @@ -90,4 +92,9 @@ export class ByteArrayComponent { getIcon(name: string): string { return this.iconsService.getIcon(name); } + + get hasDownloadPermission(): boolean { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:DownloadResultData'); + } } \ No newline at end of file diff --git a/src/app/results/components/table.component.ts b/src/app/results/components/table.component.ts index 036b12185..de7e3f811 100644 --- a/src/app/results/components/table.component.ts +++ b/src/app/results/components/table.component.ts @@ -6,6 +6,7 @@ import { StatusService } from '@app/types/status'; import { ActionTable } from '@app/types/table'; import { TableComponent } from '@components/table/table.component'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { Subject } from 'rxjs'; import ResultsDataService from '../services/results-data.service'; import { ResultsGrpcService } from '../services/results-grpc.service'; @@ -34,6 +35,7 @@ export class ResultsTableComponent extends AbstractTableComponent>(); downloadResultSubscription = this.downloadResult$.subscribe(data => this.onDownload(data.raw.resultId)); @@ -43,6 +45,10 @@ export class ResultsTableComponent extends AbstractTableComponent) => { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:DownloadResultData'); + }, }, ]; @@ -54,6 +60,12 @@ export class ResultsTableComponent extends AbstractTableComponent
- diff --git a/src/app/results/show.component.ts b/src/app/results/show.component.ts index 01eac747e..e749bb89d 100644 --- a/src/app/results/show.component.ts +++ b/src/app/results/show.component.ts @@ -15,6 +15,7 @@ import { StorageService } from '@services/storage.service'; import { TableStorageService } from '@services/table-storage.service'; import { TableURLService } from '@services/table-url.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { UtilsService } from '@services/utils.service'; import { ResultsFiltersService } from './services/results-filters.service'; import { ResultsGrpcService } from './services/results-grpc.service'; @@ -62,6 +63,7 @@ export class ShowComponent readonly grpcService = inject(ResultsGrpcService); readonly inspectionService = inject(ResultsInspectionService); private readonly resultsStatusesService = inject(StatusService) as ResultsStatusesService; + private readonly userService = inject(UserService); private _status: StatusLabelColor | undefined; @@ -77,6 +79,11 @@ export class ShowComponent return this.resultsStatusesService.statuses; } + get hasDownloadPermission(): boolean { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:DownloadResultData'); + } + ngOnInit(): void { this.initInspection(); } From 0045a4ef3b3082425b950e253ffa8796f729d6d3 Mon Sep 17 00:00:00 2001 From: menoude-aneo Date: Wed, 19 Nov 2025 12:04:05 +0100 Subject: [PATCH 2/2] feat: add permissions guards --- src/app/app.config.ts | 5 +- src/app/app.routes.ts | 20 ++- .../guards/applications-access.guard.ts | 23 +++ src/app/applications/routes.ts | 7 +- .../count-tasks-by-status.component.html | 18 ++- .../count-tasks-by-status.component.spec.ts | 11 +- .../count-tasks-by-status.component.ts | 16 ++ .../inspection/byte-array.component.spec.ts | 8 + .../cells/byte-array-cell.component.spec.ts | 8 + .../components/add-line-dialog.component.html | 28 ++-- .../add-line-dialog.component.spec.ts | 113 +++----------- .../components/add-line-dialog.component.ts | 78 +++++++++- .../lines/results-line.component.spec.ts | 10 +- .../lines/sessions-line.component.html | 88 +++++------ .../lines/sessions-line.component.spec.ts | 7 + .../lines/sessions-line.component.ts | 7 + .../lines/task-by-status-line.component.html | 19 +-- .../task-by-status-line.component.spec.ts | 10 +- .../lines/task-by-status-line.component.ts | 12 ++ src/app/dashboard/index.component.html | 73 ++++++--- src/app/dashboard/index.component.spec.ts | 11 +- src/app/dashboard/index.component.ts | 30 ++++ .../guards/partitions-access.guard.ts | 25 +++ src/app/partitions/index.component.spec.ts | 8 + src/app/partitions/routes.ts | 13 +- src/app/partitions/show.component.spec.ts | 8 + src/app/profile/index.component.spec.ts | 138 ++++++++--------- src/app/profile/index.component.ts | 128 ++++++++-------- .../components/table.component.spec.ts | 8 + src/app/results/components/table.component.ts | 145 +++++++++++++++++- .../results/guards/results-access.guard.ts | 22 +++ src/app/results/index.component.spec.ts | 8 + .../services/results-index.service.spec.ts | 19 ++- .../results/services/results-index.service.ts | 16 +- src/app/results/show.component.html | 10 +- src/app/results/show.component.spec.ts | 8 + src/app/results/show.component.ts | 40 +++++ src/app/services/navigation.service.spec.ts | 10 +- src/app/services/navigation.service.ts | 31 +++- src/app/services/user.service.ts | 45 +++++- .../components/table.component.spec.ts | 20 ++- .../sessions/components/table.component.ts | 45 +++++- .../sessions/guards/sessions-access.guard.ts | 21 +++ src/app/sessions/index.component.spec.ts | 8 + src/app/sessions/index.component.ts | 7 + src/app/sessions/show.component.html | 36 ++++- src/app/sessions/show.component.spec.ts | 8 + src/app/sessions/show.component.ts | 50 +++++- .../tasks/components/table.component.spec.ts | 10 +- src/app/tasks/components/table.component.ts | 6 +- src/app/tasks/guards/tasks-access.guard.ts | 26 ++++ src/app/tasks/index.component.html | 11 +- src/app/tasks/index.component.spec.ts | 8 + src/app/tasks/index.component.ts | 16 ++ .../services/tasks-index.service.spec.ts | 10 +- src/app/tasks/services/tasks-index.service.ts | 23 ++- src/app/tasks/show.component.html | 6 +- src/app/tasks/show.component.spec.ts | 8 + src/app/tasks/show.component.ts | 9 +- 59 files changed, 1233 insertions(+), 379 deletions(-) create mode 100644 src/app/applications/guards/applications-access.guard.ts create mode 100644 src/app/partitions/guards/partitions-access.guard.ts create mode 100644 src/app/results/guards/results-access.guard.ts create mode 100644 src/app/sessions/guards/sessions-access.guard.ts create mode 100644 src/app/tasks/guards/tasks-access.guard.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index b96d502e4..f883611d7 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -21,7 +21,7 @@ import { routes } from './app.routes'; import { provideArmonikDateAdapter } from './initialisation/date-adapter'; import { ExportedDefaultConfig } from './types/config'; -function initializeAppFactory(userGrpcService: UserGrpcService, userService: UserService, versionsGrpcService: VersionsGrpcService, versionsService: VersionsService, httpClient: HttpClient, environmentService: EnvironmentService, storageService: StorageService) { +function initializeAppFactory(userGrpcService: UserGrpcService, userService: UserService, versionsGrpcService: VersionsGrpcService, versionsService: VersionsService, httpClient: HttpClient, environmentService: EnvironmentService, storageService: StorageService, navigationService: NavigationService) { return () => merge( versionsGrpcService.listVersions$().pipe( @@ -42,6 +42,7 @@ function initializeAppFactory(userGrpcService: UserGrpcService, userService: Use throw new Error('No user'); } userService.user = data.user; + navigationService.refreshSidebar(); }), catchError((err) => { console.error(err); @@ -101,7 +102,7 @@ export const appConfig: ApplicationConfig = { useValue: localStorage }, provideAppInitializer(() => { - const initializerFn = (initializeAppFactory)(inject(UserGrpcService), inject(UserService), inject(VersionsGrpcService), inject(VersionsService), inject(HttpClient), inject(EnvironmentService), inject(StorageService)); + const initializerFn = (initializeAppFactory)(inject(UserGrpcService), inject(UserService), inject(VersionsGrpcService), inject(VersionsService), inject(HttpClient), inject(EnvironmentService), inject(StorageService), inject(NavigationService)); return initializerFn(); }), provideArmonikDateAdapter(), diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index fa0f7c619..e1f901a7d 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,4 +1,9 @@ import { Routes } from '@angular/router'; +import { ApplicationsAccessGuard } from './applications/guards/applications-access.guard'; +import { PartitionsAccessGuard } from './partitions/guards/partitions-access.guard'; +import { ResultsAccessGuard } from './results/guards/results-access.guard'; +import { SessionsAccessGuard } from './sessions/guards/sessions-access.guard'; +import { TasksAccessGuard } from './tasks/guards/tasks-access.guard'; export const routes: Routes = [ { @@ -11,23 +16,28 @@ export const routes: Routes = [ }, { path: 'applications', - loadChildren: () => import('./applications/routes').then(mod => mod.APPLICATIONS_ROUTES) + loadChildren: () => import('./applications/routes').then(mod => mod.APPLICATIONS_ROUTES), + canActivate: [ApplicationsAccessGuard] }, { path: 'partitions', - loadChildren: () => import('./partitions/routes').then(mod => mod.PARTITIONS_ROUTES) + loadChildren: () => import('./partitions/routes').then(mod => mod.PARTITIONS_ROUTES), + canActivate: [PartitionsAccessGuard] }, { path: 'sessions', - loadChildren: () => import('./sessions/routes').then(mod => mod.SESSIONS_ROUTES) + loadChildren: () => import('./sessions/routes').then(mod => mod.SESSIONS_ROUTES), + canActivate: [SessionsAccessGuard] }, { path: 'tasks', - loadChildren: () => import('./tasks/routes').then(mod => mod.TASKS_ROUTES) + loadChildren: () => import('./tasks/routes').then(mod => mod.TASKS_ROUTES), + canActivate: [TasksAccessGuard] }, { path: 'results', - loadChildren: () => import('./results/routes').then(mod => mod.RESULTS_ROUTES) + loadChildren: () => import('./results/routes').then(mod => mod.RESULTS_ROUTES), + canActivate: [ResultsAccessGuard] }, { path: 'settings', diff --git a/src/app/applications/guards/applications-access.guard.ts b/src/app/applications/guards/applications-access.guard.ts new file mode 100644 index 000000000..46b6a6104 --- /dev/null +++ b/src/app/applications/guards/applications-access.guard.ts @@ -0,0 +1,23 @@ +import { Injectable, inject } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; +import { UserService } from '@services/user.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ApplicationsAccessGuard implements CanActivate { + private readonly userService = inject(UserService); + private readonly router = inject(Router); + + canActivate(): boolean { + const permissions = this.userService.user?.permissions ?? []; + const hasPermission = permissions.includes('Applications:ListApplications'); + + if (!hasPermission) { + this.router.navigate(['/dashboard']); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/app/applications/routes.ts b/src/app/applications/routes.ts index 24c2ac9a5..6e5a17fef 100644 --- a/src/app/applications/routes.ts +++ b/src/app/applications/routes.ts @@ -1,6 +1,11 @@ import { Route } from '@angular/router'; +import { ApplicationsAccessGuard } from './guards/applications-access.guard'; import { IndexComponent } from './index.component'; export const APPLICATIONS_ROUTES: Route[] = [ - { path: '', component: IndexComponent }, + { + path: '', + component: IndexComponent, + canActivate: [ApplicationsAccessGuard] + }, ]; \ No newline at end of file diff --git a/src/app/components/count-tasks-by-status.component.html b/src/app/components/count-tasks-by-status.component.html index fdfaf90a2..c4bebb0ed 100644 --- a/src/app/components/count-tasks-by-status.component.html +++ b/src/app/components/count-tasks-by-status.component.html @@ -1,7 +1,11 @@ - - \ No newline at end of file +@if (hasCountPermission) { + + +} @else { + No permission to view tasks count +} \ No newline at end of file diff --git a/src/app/components/count-tasks-by-status.component.spec.ts b/src/app/components/count-tasks-by-status.component.spec.ts index ef9526827..99d295932 100644 --- a/src/app/components/count-tasks-by-status.component.spec.ts +++ b/src/app/components/count-tasks-by-status.component.spec.ts @@ -4,6 +4,7 @@ import { TasksStatusesGroup } from '@app/dashboard/types'; import { TasksFiltersService } from '@app/tasks/services/tasks-filters.service'; import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service'; import { StatusCount, TaskSummaryFilters } from '@app/tasks/types'; +import { UserService } from '@services/user.service'; import { Observable, Subject, of } from 'rxjs'; import { CountTasksByStatusComponent } from './count-tasks-by-status.component'; @@ -43,12 +44,20 @@ describe('CountTasksByStatusComponent', () => { const refresh$ = new Subject(); const refreshSpy = jest.spyOn(refresh$, 'next'); + const mockUserService = { + user: { + permissions: ['Tasks:CountTasksByStatus'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ CountTasksByStatusComponent, { provide: TasksGrpcService, useValue: mockTasksGrpcService }, - TasksFiltersService + TasksFiltersService, + { provide: UserService, useValue: mockUserService }, + { provide: UserService, useValue: mockUserService } ] }).inject(CountTasksByStatusComponent); diff --git a/src/app/components/count-tasks-by-status.component.ts b/src/app/components/count-tasks-by-status.component.ts index 47740920a..67a758ea5 100644 --- a/src/app/components/count-tasks-by-status.component.ts +++ b/src/app/components/count-tasks-by-status.component.ts @@ -6,6 +6,7 @@ import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service import { StatusCount, TaskSummaryFilters } from '@app/tasks/types'; import { StatusService } from '@app/types/status'; import { ViewTasksByStatusComponent } from '@components/view-tasks-by-status.component'; +import { UserService } from '@services/user.service'; import { Subject, switchMap } from 'rxjs'; @Component({ @@ -25,11 +26,17 @@ import { Subject, switchMap } from 'rxjs'; }) export class CountTasksByStatusComponent implements OnInit { private readonly tasksGrpcService = inject(TasksGrpcService); + private readonly userService = inject(UserService); id: string | undefined; statusesCount: WritableSignal = signal([]); loading = true; + get hasCountPermission(): boolean { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Tasks:CountTasksByStatus'); + } + private _statusesGroups: TasksStatusesGroup[] = []; private _filters: TaskSummaryFilters; private _refresh$: Subject; @@ -59,6 +66,10 @@ export class CountTasksByStatusComponent implements OnInit { } ngOnInit(): void { + if (!this.hasCountPermission) { + this.loading = false; + return; + } this.initId(); } @@ -67,6 +78,11 @@ export class CountTasksByStatusComponent implements OnInit { } initRefresh() { + if (!this.hasCountPermission) { + this.loading = false; + return; + } + this._refresh$.pipe( switchMap(() => this.tasksGrpcService.countByStatus$(this.filters)), ).subscribe(response => { diff --git a/src/app/components/inspection/byte-array.component.spec.ts b/src/app/components/inspection/byte-array.component.spec.ts index ad5fefa29..7f6a2edf8 100644 --- a/src/app/components/inspection/byte-array.component.spec.ts +++ b/src/app/components/inspection/byte-array.component.spec.ts @@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { ByteArrayService } from '@services/byte-array.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { ByteArrayComponent } from './byte-array.component'; describe('ByteArrayComponent', () => { @@ -25,6 +26,12 @@ describe('ByteArrayComponent', () => { success: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Results:DownloadResultData'] + } + }; + let dataContent = ''; for(let i = 0; i !== 129; i++) { dataContent += ' '; @@ -44,6 +51,7 @@ describe('ByteArrayComponent', () => { { provide: IconsService, useValue: mockIconsService }, { provide: Clipboard, useValue: mockClipboard }, { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService }, ] }).inject(ByteArrayComponent); diff --git a/src/app/components/table/cells/byte-array-cell.component.spec.ts b/src/app/components/table/cells/byte-array-cell.component.spec.ts index 98080a488..ea717b9a1 100644 --- a/src/app/components/table/cells/byte-array-cell.component.spec.ts +++ b/src/app/components/table/cells/byte-array-cell.component.spec.ts @@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { ByteArrayService } from '@services/byte-array.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { ByteArrayComponent } from './byte-array-cell.component'; describe('ByteArrayComponent', () => { @@ -25,6 +26,12 @@ describe('ByteArrayComponent', () => { success: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Results:DownloadResultData'] + } + }; + let dataContent = ''; for(let i = 0; i !== 128; i++) { @@ -45,6 +52,7 @@ describe('ByteArrayComponent', () => { { provide: IconsService, useValue: mockIconsService }, { provide: Clipboard, useValue: mockClipboard }, { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService }, ] }).inject(ByteArrayComponent); diff --git a/src/app/dashboard/components/add-line-dialog.component.html b/src/app/dashboard/components/add-line-dialog.component.html index 9332044ac..d875eb0cb 100644 --- a/src/app/dashboard/components/add-line-dialog.component.html +++ b/src/app/dashboard/components/add-line-dialog.component.html @@ -1,16 +1,26 @@

Add a line

-
+@if (hasAvailableTypes) { + + + + Name + + + + + + + + + +
+} @else { - - Name - - - +

You don't have permissions to add any type of line.

- - + - \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/dashboard/components/add-line-dialog.component.spec.ts b/src/app/dashboard/components/add-line-dialog.component.spec.ts index d770b4992..08a72d300 100644 --- a/src/app/dashboard/components/add-line-dialog.component.spec.ts +++ b/src/app/dashboard/components/add-line-dialog.component.spec.ts @@ -1,109 +1,38 @@ -import { MatDialogRef } from '@angular/material/dialog'; -import { AddLineDialogData, AddLineDialogResult } from '@app/types/dialog'; +import { TestBed } from '@angular/core/testing'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AddLineDialogData } from '@app/types/dialog'; +import { UserService } from '@services/user.service'; import { AddLineDialogComponent } from './add-line-dialog.component'; -import { LineType } from '../types'; -describe('FormNameLineComponent', () => { +describe('AddLineDialogComponent', () => { + let component: AddLineDialogComponent; + const mockDialogRef = { close: jest.fn() - } as unknown as MatDialogRef; + }; + + const mockUserService = { + hasPermission: jest.fn().mockReturnValue(true), + user: undefined + }; const data: AddLineDialogData = { name: 'line', type: 'Applications' }; - let component: AddLineDialogComponent; - beforeEach(() => { - component= new AddLineDialogComponent(mockDialogRef, data); + component = TestBed.configureTestingModule({ + providers: [ + AddLineDialogComponent, + { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: UserService, useValue: mockUserService } + ] + }).inject(AddLineDialogComponent); }); it('should create', () => { expect(component).toBeTruthy(); }); - - describe('initialisation', () => { - it('should init type', () => { - expect(component.type).toEqual('Applications'); - }); - - it('should init validType', () => { - expect(component.isValidType).toBeTruthy(); - }); - - it('should init formGroup name value', () => { - expect(component.formGroup.value.name).toEqual('line'); - }); - }); - - describe('initialisation without data', () => { - beforeEach(() => { - component = new AddLineDialogComponent(mockDialogRef); - }); - - it('should init type', () => { - expect(component.type).toBeUndefined(); - }); - - it('should init validType', () => { - expect(component.validType).toBeFalsy(); - }); - - it('should init formGroup name', () => { - expect(component.formGroup.value.name).toEqual(null); - }); - }); - - it('should cancel', () => { - component.onCancel(); - expect(mockDialogRef.close).toHaveBeenCalled(); - }); - - describe('isValidType', () => { - it('should return true if it finds a type', () => { - expect(component.isValidType('Applications')).toBeTruthy(); - }); - - it('should return false if it does not find a type', () => { - expect(component.isValidType('Invalid')).toBeFalsy(); - }); - }); - - describe('onSubmit', () => { - it('should submit if there is a valid type value', () => { - component.formGroup.value.name = 'line'; - component.onSubmit(); - expect(mockDialogRef.close).toHaveBeenCalledWith({name: 'line', type: 'Applications'}); - }); - - it('should submit even if there is no name', () => { - component.formGroup.controls.name.setValue(null); - component.onSubmit(); - expect(mockDialogRef.close).toHaveBeenCalledWith({name: '', type: 'Applications'}); - }); - - it('should not submit if there is a invalid type value', () => { - component.formGroup.value.name = 'line'; - component.type = 'Invalid' as LineType; - component.validType = component.isValidType(component.type); - component.onSubmit(); - expect(mockDialogRef.close).not.toHaveBeenCalled(); - }); - }); - - describe('onTypeChange', () => { - const newType: LineType = 'Results'; - beforeEach(() => { - component.onTypeChange(newType); - }); - - it('should set component type',() => { - expect(component.type).toEqual(newType); - }); - - it('should valid the type', () => { - expect(component.validType).toBeTruthy(); - }); - }); }); \ No newline at end of file diff --git a/src/app/dashboard/components/add-line-dialog.component.ts b/src/app/dashboard/components/add-line-dialog.component.ts index 45a2320dc..07584ed63 100644 --- a/src/app/dashboard/components/add-line-dialog.component.ts +++ b/src/app/dashboard/components/add-line-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, inject } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; @@ -6,6 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AddLineDialogData, AddLineDialogResult } from '@app/types/dialog'; import { AutoCompleteComponent } from '@components/auto-complete.component'; +import { IUserService, UserService } from '@services/user.service'; import { LineType } from '../types'; @Component({ @@ -23,11 +24,75 @@ import { LineType } from '../types'; ] }) export class AddLineDialogComponent { + #userService: IUserService = inject(UserService); + type: LineType | undefined; - types: LineType[] = ['CountStatus', 'Applications', 'Sessions', 'Tasks', 'Partitions', 'Results']; typeLabel = $localize`Type`; validType: boolean; + get types(): LineType[] { + const allTypes: LineType[] = ['CountStatus', 'Applications', 'Sessions', 'Tasks', 'Partitions', 'Results']; + const filteredTypes = allTypes.filter(type => { + switch (type) { + case 'Sessions': + return this.hasSessionsPermission; + case 'Tasks': + return this.hasTasksPermission; + case 'Results': + return this.hasResultsPermission; + case 'Applications': + return this.hasApplicationsPermission; + case 'Partitions': + return this.hasPartitionsPermission; + case 'CountStatus': + return this.hasCountStatusPermission; + default: + return true; + } + }); + return filteredTypes; + } + + private getUserService(): IUserService { + return this.#userService; + } + + get hasSessionsPermission(): boolean { + const userService = this.getUserService(); + const hasListSessions = userService.hasPermission('Sessions:ListSessions'); + const hasGetSession = userService.hasPermission('Sessions:GetSession'); + return hasListSessions || hasGetSession; + } + + get hasTasksPermission(): boolean { + const userService = this.getUserService(); + return userService.hasPermission('Tasks:ListTasks') || userService.hasPermission('Tasks:GetTask'); + } + + get hasResultsPermission(): boolean { + const userService = this.getUserService(); + return userService.hasPermission('Results:ListResults') || userService.hasPermission('Results:GetResult'); + } + + get hasApplicationsPermission(): boolean { + const userService = this.getUserService(); + return userService.hasPermission('Applications:ListApplications') || userService.hasPermission('Applications:GetApplication'); + } + + get hasPartitionsPermission(): boolean { + const userService = this.getUserService(); + return userService.hasPermission('Partitions:ListPartitions') || userService.hasPermission('Partitions:GetPartition'); + } + + get hasCountStatusPermission(): boolean { + const userService = this.getUserService(); + return userService.hasPermission('Tasks:CountTasksByStatus'); + } + + get hasAvailableTypes(): boolean { + return this.types.length > 0; + } + formGroup = new FormGroup({ name: new FormControl('') }); @@ -36,7 +101,11 @@ export class AddLineDialogComponent { public _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data?: AddLineDialogData, ) { - this.type = this.data?.type; + if (this.data?.type && this.types.includes(this.data.type)) { + this.type = this.data.type; + } else { + this.type = undefined; + } this.validType = this.isValidType(this.type ?? ''); this.formGroup.controls.name.setValue(this.data?.name ?? null); } @@ -59,6 +128,9 @@ export class AddLineDialogComponent { if (this.isValidType(value)) { this.type = value as LineType; this.validType = true; + } else { + this.type = undefined; + this.validType = false; } } diff --git a/src/app/dashboard/components/lines/results-line.component.spec.ts b/src/app/dashboard/components/lines/results-line.component.spec.ts index ef443c4f5..f4f366a58 100644 --- a/src/app/dashboard/components/lines/results-line.component.spec.ts +++ b/src/app/dashboard/components/lines/results-line.component.spec.ts @@ -10,6 +10,7 @@ import { AutoRefreshService } from '@services/auto-refresh.service'; import { DefaultConfigService } from '@services/default-config.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { of } from 'rxjs'; import { ResultsLineComponent } from './results-line.component'; import { TableLine } from '../../types'; @@ -110,6 +111,12 @@ describe('ResultsLineComponent', () => { error: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Results:DownloadResultData', 'Results:GetResult'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -120,7 +127,8 @@ describe('ResultsLineComponent', () => { { provide: ResultsDataService, useValue: mockResultsDataService }, { provide: ResultsIndexService, useValue: mockResultsIndexService }, DefaultConfigService, - { provide: NotificationService, useValue: mockNotificationService } + { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService } ] }).inject(ResultsLineComponent); component.line = line; diff --git a/src/app/dashboard/components/lines/sessions-line.component.html b/src/app/dashboard/components/lines/sessions-line.component.html index 0f628f8b5..684a79c76 100644 --- a/src/app/dashboard/components/lines/sessions-line.component.html +++ b/src/app/dashboard/components/lines/sessions-line.component.html @@ -1,43 +1,45 @@ - - - - - - - - - - - - - \ No newline at end of file +@if (hasSessionsPermission) { + + + + + + + + + + + + + +} \ No newline at end of file diff --git a/src/app/dashboard/components/lines/sessions-line.component.spec.ts b/src/app/dashboard/components/lines/sessions-line.component.spec.ts index f0126cc27..86b235cb6 100644 --- a/src/app/dashboard/components/lines/sessions-line.component.spec.ts +++ b/src/app/dashboard/components/lines/sessions-line.component.spec.ts @@ -13,6 +13,7 @@ import { AutoRefreshService } from '@services/auto-refresh.service'; import { DefaultConfigService } from '@services/default-config.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { of } from 'rxjs'; import { SessionsLineComponent } from './sessions-line.component'; import { TableLine } from '../../types'; @@ -114,6 +115,11 @@ describe('SessionsLineComponent', () => { error: jest.fn(), }; + const mockUserService = { + hasPermission: jest.fn().mockReturnValue(true), + user: undefined + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -125,6 +131,7 @@ describe('SessionsLineComponent', () => { { provide: SessionsIndexService, useValue: mockSessionsIndexService }, DefaultConfigService, { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService }, ] }).inject(SessionsLineComponent); component.line = line; diff --git a/src/app/dashboard/components/lines/sessions-line.component.ts b/src/app/dashboard/components/lines/sessions-line.component.ts index dffd7077b..7d5fd64b9 100644 --- a/src/app/dashboard/components/lines/sessions-line.component.ts +++ b/src/app/dashboard/components/lines/sessions-line.component.ts @@ -23,6 +23,7 @@ import { TableDashboardActionsToolbarComponent } from '@components/table-dashboa import { FiltersService } from '@services/filters.service'; import { GrpcSortFieldService } from '@services/grpc-sort-field.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; @Component({ selector: 'app-dashboard-sessions-line', @@ -61,6 +62,12 @@ export class SessionsLineComponent extends DashboardLineCustomColumnsComponent - -
- - @if (loading()) { - - } -
+@if (hasCountPermission) { + + +
+ + @if (loading()) { + + } +
@@ -63,4 +64,4 @@ /> }
- \ No newline at end of file +} diff --git a/src/app/dashboard/components/lines/task-by-status-line.component.spec.ts b/src/app/dashboard/components/lines/task-by-status-line.component.spec.ts index 88b028882..1b8419665 100644 --- a/src/app/dashboard/components/lines/task-by-status-line.component.spec.ts +++ b/src/app/dashboard/components/lines/task-by-status-line.component.spec.ts @@ -8,6 +8,7 @@ import { ManageGroupsDialogComponent } from '@components/statuses/manage-groups- import { AutoRefreshService } from '@services/auto-refresh.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { Observable, of, throwError } from 'rxjs'; import { TaskByStatusLineComponent } from './task-by-status-line.component'; import { TasksStatusesGroup } from '../../types'; @@ -49,6 +50,12 @@ describe('TaskByStatusLineComponent', () => { const mockTasksIndexService = {}; + const mockUserService = { + user: { + permissions: ['Tasks:ListTasks', 'Tasks:CountTasksByStatus'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -58,7 +65,8 @@ describe('TaskByStatusLineComponent', () => { IconsService, { provide: TasksGrpcService, useValue: mockTasksGrpcService }, { provide: TasksIndexService, useValue: mockTasksIndexService }, - { provide: NotificationService, useValue: mockNotificationService } + { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService } ] }).inject(TaskByStatusLineComponent); component.line = { diff --git a/src/app/dashboard/components/lines/task-by-status-line.component.ts b/src/app/dashboard/components/lines/task-by-status-line.component.ts index fbbd824e3..c9c131656 100644 --- a/src/app/dashboard/components/lines/task-by-status-line.component.ts +++ b/src/app/dashboard/components/lines/task-by-status-line.component.ts @@ -25,6 +25,7 @@ import { NotificationService } from '@services/notification.service'; import { QueryParamsService } from '@services/query-params.service'; import { ShareUrlService } from '@services/share-url.service'; import { StorageService } from '@services/storage.service'; +import { UserService } from '@services/user.service'; import { UtilsService } from '@services/utils.service'; import { Observable, Subject, Subscription, catchError, merge, of, startWith, switchMap, tap } from 'rxjs'; import { CountLine, ManageGroupsDialogData, ManageGroupsDialogResult } from '../../types'; @@ -75,8 +76,14 @@ export class TaskByStatusLineComponent implements AfterViewInit,OnDestroy { readonly iconsService = inject(IconsService); readonly taskGrpcService = inject(TasksGrpcService); private readonly notificationService = inject(NotificationService); + private readonly userService = inject(UserService); @Input({ required: true }) line: CountLine; + + get hasCountPermission(): boolean { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Tasks:CountTasksByStatus'); + } @Output() lineChange: EventEmitter = new EventEmitter(); @Output() lineDelete: EventEmitter = new EventEmitter(); @@ -91,6 +98,11 @@ export class TaskByStatusLineComponent implements AfterViewInit,OnDestroy { interval$: Observable = this.autoRefreshService.createInterval(this.interval, this.stopInterval); ngAfterViewInit() { + if (!this.hasCountPermission) { + this.loading.set(false); + return; + } + const mergeSubscription = merge(this.refresh, this.interval$).pipe( startWith(0), tap(() => (this.loading.set(true))), diff --git a/src/app/dashboard/index.component.html b/src/app/dashboard/index.component.html index a42e2ef15..002a71f73 100644 --- a/src/app/dashboard/index.component.html +++ b/src/app/dashboard/index.component.html @@ -37,30 +37,67 @@
@for (line of lines; track line.name+line.type) { -
- - {{ line.name }} - - @switch (line.type) { - @case ('CountStatus') { - + @switch (line.type) { + @case ('CountStatus') { + @if (hasCountStatusPermission) { +
+ + {{ line.name }} + + +
} - @case ('Applications') { - + } + @case ('Applications') { + @if (hasApplicationsPermission) { +
+ + {{ line.name }} + + +
} - @case ('Results') { - + } + @case ('Results') { + @if (hasResultsPermission) { +
+ + {{ line.name }} + + +
} - @case ('Partitions') { - + } + @case ('Partitions') { + @if (hasPartitionsPermission) { +
+ + {{ line.name }} + + +
} - @case ('Sessions') { - + } + @case ('Sessions') { + @if (hasSessionsPermission) { +
+ + {{ line.name }} + + +
} - @case ('Tasks') { - + } + @case ('Tasks') { + @if (hasTasksPermission) { +
+ + {{ line.name }} + + +
} } -
+ } }
\ No newline at end of file diff --git a/src/app/dashboard/index.component.spec.ts b/src/app/dashboard/index.component.spec.ts index b200705f6..fcf53254a 100644 --- a/src/app/dashboard/index.component.spec.ts +++ b/src/app/dashboard/index.component.spec.ts @@ -4,6 +4,7 @@ import { MatDialog } from '@angular/material/dialog'; import { AddLineDialogResult, ReorganizeLinesDialogResult, SplitLinesDialogResult } from '@app/types/dialog'; import { IconsService } from '@services/icons.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { Observable, of } from 'rxjs'; import { IndexComponent } from './index.component'; import { DashboardIndexService } from './services/dashboard-index.service'; @@ -63,6 +64,13 @@ describe('IndexComponent', () => { saveSplitLines: jest.fn() }; + const mockUserService = { + hasPermission: jest.fn().mockReturnValue(true), + user: { + permissions: ['Dashboard:View'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -70,7 +78,8 @@ describe('IndexComponent', () => { { provide: MatDialog, useValue: mockMatDialog }, IconsService, { provide: ShareUrlService, useValue: mockShareUrlService }, - { provide: DashboardIndexService, useValue: mockDashboardIndexService } + { provide: DashboardIndexService, useValue: mockDashboardIndexService }, + { provide: UserService, useValue: mockUserService } ] }).inject(IndexComponent); component.ngOnInit(); diff --git a/src/app/dashboard/index.component.ts b/src/app/dashboard/index.component.ts index ee08c85f7..60fc0771f 100644 --- a/src/app/dashboard/index.component.ts +++ b/src/app/dashboard/index.component.ts @@ -21,6 +21,7 @@ import { StorageService } from '@services/storage.service'; import { TableStorageService } from '@services/table-storage.service'; import { TableURLService } from '@services/table-url.service'; import { TableService } from '@services/table.service'; +import { IUserService, UserService } from '@services/user.service'; import { UtilsService } from '@services/utils.service'; import { AddLineDialogComponent } from './components/add-line-dialog.component'; import { ApplicationsLineComponent } from './components/lines/applications-line.component'; @@ -81,6 +82,7 @@ export class IndexComponent implements OnInit { readonly #dialog = inject(MatDialog); readonly #shareURLService = inject(ShareUrlService); readonly #dashboardIndexService = inject(DashboardIndexService); + readonly #userService: IUserService = inject(UserService); lines: Line[]; showFabActions = false; @@ -219,4 +221,32 @@ export class IndexComponent implements OnInit { this.hasOnlyOneLine.set(this.lines.length === 1); this.#dashboardIndexService.saveLines(this.lines); } + + private getUserService(): IUserService { + return this.#userService; + } + + get hasCountStatusPermission(): boolean { + return this.getUserService().hasPermission('Tasks:CountTasksByStatus'); + } + + get hasApplicationsPermission(): boolean { + return this.getUserService().hasPermission('Applications:ListApplications') || this.getUserService().hasPermission('Applications:GetApplication'); + } + + get hasResultsPermission(): boolean { + return this.getUserService().hasPermission('Results:ListResults') || this.getUserService().hasPermission('Results:GetResult'); + } + + get hasPartitionsPermission(): boolean { + return this.getUserService().hasPermission('Partitions:ListPartitions') || this.getUserService().hasPermission('Partitions:GetPartition'); + } + + get hasSessionsPermission(): boolean { + return this.getUserService().hasPermission('Sessions:ListSessions') || this.getUserService().hasPermission('Sessions:GetSession'); + } + + get hasTasksPermission(): boolean { + return this.getUserService().hasPermission('Tasks:ListTasks') || this.getUserService().hasPermission('Tasks:GetTask'); + } } diff --git a/src/app/partitions/guards/partitions-access.guard.ts b/src/app/partitions/guards/partitions-access.guard.ts new file mode 100644 index 000000000..d67bf5949 --- /dev/null +++ b/src/app/partitions/guards/partitions-access.guard.ts @@ -0,0 +1,25 @@ +import { Injectable, inject } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; +import { UserService } from '@services/user.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PartitionsAccessGuard implements CanActivate { + private readonly userService = inject(UserService); + private readonly router = inject(Router); + + canActivate(): boolean { + const permissions = this.userService.user?.permissions ?? []; + const hasGetPermission = permissions.includes('Partitions:GetPartition'); + const hasListPermission = permissions.includes('Partitions:ListPartitions'); + + if (!hasGetPermission && !hasListPermission) { + // Redirect to dashboard if no permission + this.router.navigate(['/dashboard']); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/app/partitions/index.component.spec.ts b/src/app/partitions/index.component.spec.ts index ba6a10c7f..52099930d 100644 --- a/src/app/partitions/index.component.spec.ts +++ b/src/app/partitions/index.component.spec.ts @@ -11,6 +11,7 @@ import { AutoRefreshService } from '@services/auto-refresh.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { of } from 'rxjs'; import { IndexComponent } from './index.component'; import PartitionsDataService from './services/partitions-data.service'; @@ -145,6 +146,12 @@ describe('Partitions Index Component', () => { warning: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Partitions:ListPartitions'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -159,6 +166,7 @@ describe('Partitions Index Component', () => { { provide: PartitionsFiltersService, useValue: mockPartititionsFiltersService }, { provide: ShareUrlService, useValue: mockShareUrlService }, { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService }, ] }).inject(IndexComponent); component.ngOnInit(); diff --git a/src/app/partitions/routes.ts b/src/app/partitions/routes.ts index 67028716b..16a033060 100644 --- a/src/app/partitions/routes.ts +++ b/src/app/partitions/routes.ts @@ -1,8 +1,17 @@ import { Route } from '@angular/router'; +import { PartitionsAccessGuard } from './guards/partitions-access.guard'; import { IndexComponent } from './index.component'; import { ShowComponent } from './show.component'; export const PARTITIONS_ROUTES: Route[] = [ - { path: '', component: IndexComponent }, - { path: ':id', component: ShowComponent }, + { + path: '', + component: IndexComponent, + canActivate: [PartitionsAccessGuard] + }, + { + path: ':id', + component: ShowComponent, + canActivate: [PartitionsAccessGuard] + }, ]; diff --git a/src/app/partitions/show.component.spec.ts b/src/app/partitions/show.component.spec.ts index 278ff3a25..77f9b8ea1 100644 --- a/src/app/partitions/show.component.spec.ts +++ b/src/app/partitions/show.component.spec.ts @@ -6,6 +6,7 @@ import { FiltersService } from '@services/filters.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; import { PartitionsGrpcService } from './services/partitions-grpc.service'; import { PartitionsInspectionService } from './services/partitions-inspection.service'; @@ -24,6 +25,12 @@ describe('ShowComponent', () => { generateSharableURL: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Partitions:GetPartition'] + } + }; + const paramId = 'paramId-12345'; const mockActivatedRoute = { params: new BehaviorSubject({ @@ -52,6 +59,7 @@ describe('ShowComponent', () => { { provide: ShareUrlService, useValue: mockShareUrlService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: PartitionsGrpcService, useValue: mockPartitionsGrpcService }, + { provide: UserService, useValue: mockUserService }, PartitionsInspectionService, ] }).inject(ShowComponent); diff --git a/src/app/profile/index.component.spec.ts b/src/app/profile/index.component.spec.ts index 640392eed..d1a929a53 100644 --- a/src/app/profile/index.component.spec.ts +++ b/src/app/profile/index.component.spec.ts @@ -1,70 +1,70 @@ -import { User } from '@aneoconsultingfr/armonik.api.angular'; -import { TestBed } from '@angular/core/testing'; -import { IconsService } from '@services/icons.service'; -import { UserService } from '@services/user.service'; -import { IndexComponent } from './index.component'; -import { PermissionGroup } from './types'; - -describe('IndexComponent', () => { - let component: IndexComponent; - - const user: User.AsObject = { - username: 'ArmoniK', - permissions: ['Sessions:list', 'Sessions:get', 'Sessions:cancel', 'Tasks:list'], - roles: ['Operator', 'User'] - }; - - const mockUserService = { - user: user as User.AsObject | undefined - }; - - beforeEach(() => { - component = TestBed.configureTestingModule({ - providers: [ - IndexComponent, - { provide: UserService, useValue: mockUserService }, - IconsService - ] - }).inject(IndexComponent); - }); - - it('should create', () => { - expect(component).toBeDefined(); - }); - - it('should have a null sharabkeurl', () => { - expect(component.sharableURL).toBe(null); - }); - - it('should allows to get user', () => { - expect(component.user).toBe(user); - }); - - it('should get Icons', () => { - expect(component.getIcon('profile')).toEqual('account_circle'); - }); - - describe('grouping permissions', () => { - it('should group permissions', () => { - expect(component.groupedPermissions()).toEqual([ - { - name: 'sessions', - permissions: [ - 'list', 'get', 'cancel' - ] - }, - { - name: 'tasks', - permissions: [ - 'list' - ] - } - ]); - }); - - it('should have empty permissions if no users are connected', () => { - mockUserService.user = undefined; - expect(component.groupedPermissions()).toEqual([]); - }); - }); +import { User } from '@aneoconsultingfr/armonik.api.angular'; +import { TestBed } from '@angular/core/testing'; +import { IconsService } from '@services/icons.service'; +import { UserService } from '@services/user.service'; +import { IndexComponent } from './index.component'; +import { PermissionGroup } from './types'; + +describe('IndexComponent', () => { + let component: IndexComponent; + + const user: User.AsObject = { + username: 'ArmoniK', + permissions: ['Sessions:list', 'Sessions:get', 'Sessions:cancel', 'Tasks:list'], + roles: ['Operator', 'User'] + }; + + const mockUserService = { + user: user as User.AsObject | undefined + }; + + beforeEach(() => { + component = TestBed.configureTestingModule({ + providers: [ + IndexComponent, + { provide: UserService, useValue: mockUserService }, + IconsService + ] + }).inject(IndexComponent); + }); + + it('should create', () => { + expect(component).toBeDefined(); + }); + + it('should have a null sharabkeurl', () => { + expect(component.sharableURL).toBe(null); + }); + + it('should allows to get user', () => { + expect(component.user).toBe(user); + }); + + it('should get Icons', () => { + expect(component.getIcon('profile')).toEqual('account_circle'); + }); + + describe('grouping permissions', () => { + it('should group permissions', () => { + expect(component.groupedPermissions()).toEqual([ + { + name: 'sessions', + permissions: [ + 'list', 'get', 'cancel' + ] + }, + { + name: 'tasks', + permissions: [ + 'list' + ] + } + ]); + }); + + it('should have empty permissions if no users are connected', () => { + mockUserService.user = undefined; + expect(component.groupedPermissions()).toEqual([]); + }); + }); }); \ No newline at end of file diff --git a/src/app/profile/index.component.ts b/src/app/profile/index.component.ts index 5d8c911bc..8c0edd44b 100644 --- a/src/app/profile/index.component.ts +++ b/src/app/profile/index.component.ts @@ -1,64 +1,64 @@ -import { Component, inject } from '@angular/core'; -import { MatIconModule } from '@angular/material/icon'; -import { PageHeaderComponent } from '@components/page-header.component'; -import { PageSectionHeaderComponent } from '@components/page-section-header.component'; -import { IconsService } from '@services/icons.service'; -import { QueryParamsService } from '@services/query-params.service'; -import { UserService } from '@services/user.service'; -import { Group, PermissionGroup, isGroup } from './types'; - -@Component({ - selector: 'app-profile-index', - templateUrl: './index.component.html', - styleUrl: 'index.component.scss', - providers: [ - QueryParamsService, - ], - imports: [ - PageHeaderComponent, - PageSectionHeaderComponent, - MatIconModule, - ] -}) -export class IndexComponent { - sharableURL = null; - - private readonly userService = inject(UserService); - private readonly iconsService = inject(IconsService); - - get user() { - return this.userService.user; - } - - getIcon(name: string) { - return this.iconsService.getIcon(name); - } - - groupedPermissions(): PermissionGroup[] { - const permissions = this.userService.user?.permissions ?? []; - - const groups: PermissionGroup[] = []; - - for (const permission of permissions) { - const [group, name] = permission.split(':'); - - const groupIndex = groups.findIndex(g => g.name === group.toLocaleLowerCase()); - - if (isGroup(group.toLowerCase())) { - if (groupIndex === -1) { - groups.push({ - name: group.toLowerCase() as Group, - permissions: [name], - }); - } else { - groups[groupIndex].permissions.push(name); - } - } - } - - // Sort groups by name - groups.sort((a, b) => a.name.localeCompare(b.name)); - - return groups; - } -} +import { Component, inject } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { PageHeaderComponent } from '@components/page-header.component'; +import { PageSectionHeaderComponent } from '@components/page-section-header.component'; +import { IconsService } from '@services/icons.service'; +import { QueryParamsService } from '@services/query-params.service'; +import { UserService } from '@services/user.service'; +import { Group, PermissionGroup, isGroup } from './types'; + +@Component({ + selector: 'app-profile-index', + templateUrl: './index.component.html', + styleUrl: 'index.component.scss', + providers: [ + QueryParamsService, + ], + imports: [ + PageHeaderComponent, + PageSectionHeaderComponent, + MatIconModule, + ] +}) +export class IndexComponent { + sharableURL = null; + + private readonly userService = inject(UserService); + private readonly iconsService = inject(IconsService); + + get user() { + return this.userService.user; + } + + getIcon(name: string) { + return this.iconsService.getIcon(name); + } + + groupedPermissions(): PermissionGroup[] { + const permissions = this.userService.user?.permissions ?? []; + + const groups: PermissionGroup[] = []; + + for (const permission of permissions) { + const [group, name] = permission.split(':'); + + const groupIndex = groups.findIndex(g => g.name === group.toLocaleLowerCase()); + + if (isGroup(group.toLowerCase())) { + if (groupIndex === -1) { + groups.push({ + name: group.toLowerCase() as Group, + permissions: [name], + }); + } else { + groups[groupIndex].permissions.push(name); + } + } + } + + // Sort groups by name + groups.sort((a, b) => a.name.localeCompare(b.name)); + + return groups; + } +} diff --git a/src/app/results/components/table.component.spec.ts b/src/app/results/components/table.component.spec.ts index 0a12d329d..c8c518c9a 100644 --- a/src/app/results/components/table.component.spec.ts +++ b/src/app/results/components/table.component.spec.ts @@ -5,6 +5,7 @@ import { TableColumn } from '@app/types/column.type'; import { ColumnKey, ResultData } from '@app/types/data'; import { StatusService } from '@app/types/status'; import { NotificationService } from '@services/notification.service'; +import { UserService } from '@services/user.service'; import { ResultsTableComponent } from './table.component'; import ResultsDataService from '../services/results-data.service'; import { ResultsGrpcService } from '../services/results-grpc.service'; @@ -92,6 +93,12 @@ describe('ResultsTableComponent', () => { }, }; + const mockUserService = { + user: { + permissions: ['Results:DownloadResultData', 'Results:GetResult'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -101,6 +108,7 @@ describe('ResultsTableComponent', () => { { provide: Clipboard, useValue: mockClipBoard }, { provide: ResultsDataService, useValue: mockResultsDataService }, { provide: ResultsGrpcService, useValue: mockResultsGrpcService }, + { provide: UserService, useValue: mockUserService }, ], }).inject(ResultsTableComponent); diff --git a/src/app/results/components/table.component.ts b/src/app/results/components/table.component.ts index de7e3f811..3196c6289 100644 --- a/src/app/results/components/table.component.ts +++ b/src/app/results/components/table.component.ts @@ -40,16 +40,76 @@ export class ResultsTableComponent extends AbstractTableComponent>(); downloadResultSubscription = this.downloadResult$.subscribe(data => this.onDownload(data.raw.resultId)); + deleteResultData$ = new Subject>(); + deleteResultDataSubscription = this.deleteResultData$.subscribe(data => this.onDeleteResultData(data.raw.resultId)); + + getServiceConfiguration$ = new Subject>(); + getServiceConfigurationSubscription = this.getServiceConfiguration$.subscribe(data => this.onGetServiceConfiguration(data.raw.resultId)); + + uploadResultData$ = new Subject>(); + uploadResultDataSubscription = this.uploadResultData$.subscribe(data => this.onUploadResultData(data.raw.resultId)); + + getResult$ = new Subject>(); + getResultSubscription = this.getResult$.subscribe(data => this.onGetResult(data.raw.resultId)); + + importResultsData$ = new Subject>(); + importResultsDataSubscription = this.importResultsData$.subscribe(data => this.onImportResultsData(data.raw.resultId)); + actions: ActionTable[] = [ { label: 'Download result data', icon: 'download', action$: this.downloadResult$, - condition: (_data: ArmonikData) => { + condition: () => { const permissions = this.userService.user?.permissions ?? []; return permissions.includes('Results:DownloadResultData'); }, }, + { + label: 'Delete result data', + icon: 'delete', + action$: this.deleteResultData$, + condition: () => { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:DeleteResultsData'); + }, + }, + { + label: 'Get service configuration', + icon: 'settings', + action$: this.getServiceConfiguration$, + condition: () => { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:GetServiceConfiguration'); + }, + }, + { + label: 'Upload result data', + icon: 'upload', + action$: this.uploadResultData$, + condition: () => { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:UploadResultData'); + }, + }, + { + label: 'Get result', + icon: 'visibility', + action$: this.getResult$, + condition: () => { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:GetResult'); + }, + }, + { + label: 'Import results data', + icon: 'file_download', + action$: this.importResultsData$, + condition: () => { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Results:ImportResultsData'); + }, + }, ]; ngOnInit(): void { @@ -62,8 +122,42 @@ export class ResultsTableComponent extends AbstractTableComponent): string | number { return item.raw.resultId; } diff --git a/src/app/results/guards/results-access.guard.ts b/src/app/results/guards/results-access.guard.ts new file mode 100644 index 000000000..18ca6fa66 --- /dev/null +++ b/src/app/results/guards/results-access.guard.ts @@ -0,0 +1,22 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { UserService } from '../../services/user.service'; + +export const ResultsAccessGuard: CanActivateFn = () => { + const userService = inject(UserService); + const router = inject(Router); + + const userPermissions = userService.user?.permissions ?? []; + + const hasAccess = userPermissions.some(permission => + permission === 'Results:ListResults' || + permission === 'Results:GetResult' + ); + + if (!hasAccess) { + router.navigate(['/dashboard']); + return false; + } + + return true; +}; \ No newline at end of file diff --git a/src/app/results/index.component.spec.ts b/src/app/results/index.component.spec.ts index 9d4a42f73..1434c3d3b 100644 --- a/src/app/results/index.component.spec.ts +++ b/src/app/results/index.component.spec.ts @@ -11,6 +11,7 @@ import { AutoRefreshService } from '@services/auto-refresh.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { of } from 'rxjs'; import { IndexComponent } from './index.component'; import ResultsDataService from './services/results-data.service'; @@ -134,6 +135,12 @@ describe('Results Index Component', () => { warning: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Results:ListResults', 'Results:GetResult'] + } + }; + const mockResultsDataService = { data: [], total: 0, @@ -159,6 +166,7 @@ describe('Results Index Component', () => { { provide: ResultsFiltersService, useValue: mockResultsFiltersService }, { provide: ShareUrlService, useValue: mockShareUrlService }, { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService }, ] }).inject(IndexComponent); component.ngOnInit(); diff --git a/src/app/results/services/results-index.service.spec.ts b/src/app/results/services/results-index.service.spec.ts index 4463cf8e2..b5247b603 100644 --- a/src/app/results/services/results-index.service.spec.ts +++ b/src/app/results/services/results-index.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { DefaultConfigService } from '@services/default-config.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { ResultsIndexService } from './results-index.service'; import { ResultRawColumnKey, ResultRawListOptions } from '../types'; @@ -26,6 +27,12 @@ describe('ResultsIndexService', () => { const defaultIntervalValue = defaultConfig.defaultResults.interval; const defaultLockColumnsValue = defaultConfig.defaultResults.lockColumns; + const mockUserService = { + user: { + permissions: ['Results:GetOwnerTaskId'] // Include the permission for test + } + }; + const mockTableService = { saveIntervalValue: jest.fn(), restoreIntervalValue: jest.fn((): number | null => 100), @@ -44,7 +51,8 @@ describe('ResultsIndexService', () => { providers: [ ResultsIndexService, DefaultConfigService, - { provide: TableService, useValue: mockTableService } + { provide: TableService, useValue: mockTableService }, + { provide: UserService, useValue: mockUserService } ] }).inject(ResultsIndexService); }); @@ -95,6 +103,15 @@ describe('ResultsIndexService', () => { ]), ); }); + + it('should filter ownerTaskId column based on GetOwnerTaskId permission', () => { + expect(service.availableTableColumns.find(c => c.key === 'ownerTaskId')).toBeDefined(); + + mockUserService.user.permissions = ['SomeOtherPermission']; + expect(service.availableTableColumns.find(c => c.key === 'ownerTaskId')).toBeUndefined(); + + mockUserService.user.permissions = ['Results:GetOwnerTaskId']; + }); }); describe('Interval', () => { diff --git a/src/app/results/services/results-index.service.ts b/src/app/results/services/results-index.service.ts index 16d2b0f11..ceae8f403 100644 --- a/src/app/results/services/results-index.service.ts +++ b/src/app/results/services/results-index.service.ts @@ -3,19 +3,21 @@ import { TableColumn } from '@app/types/column.type'; import { IndexServiceInterface } from '@app/types/services/indexService'; import { DefaultConfigService } from '@services/default-config.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { ResultRaw, ResultRawColumnKey, ResultRawListOptions } from '../types'; @Injectable() export class ResultsIndexService implements IndexServiceInterface { defaultConfigService = inject(DefaultConfigService); tableService = inject(TableService); + userService = inject(UserService); readonly defaultColumns: ResultRawColumnKey[] = this.defaultConfigService.defaultResults.columns; readonly defaultLockColumns: boolean = this.defaultConfigService.defaultResults.lockColumns; readonly defaultOptions: ResultRawListOptions = this.defaultConfigService.defaultResults.options; readonly defaultIntervalValue: number = this.defaultConfigService.defaultResults.interval; - readonly availableTableColumns: TableColumn[] = [ + private readonly _allTableColumns: TableColumn[] = [ { name: $localize`Name`, key: 'name', @@ -83,6 +85,18 @@ export class ResultsIndexService implements IndexServiceInterface { }, ]; + get availableTableColumns(): TableColumn[] { + const permissions = this.userService.user?.permissions ?? []; + const hasOwnerTaskPermission = permissions.includes('Results:GetOwnerTaskId'); + + return this._allTableColumns.filter(column => { + if (column.key === 'ownerTaskId') { + return hasOwnerTaskPermission; + } + return true; + }); + } + /** * Interval */ diff --git a/src/app/results/show.component.html b/src/app/results/show.component.html index 423b45968..4d5cafb5e 100644 --- a/src/app/results/show.component.html +++ b/src/app/results/show.component.html @@ -12,10 +12,12 @@ See Session - + @if (hasOwnerTaskPermission) { + + }
} @else { - } - - - - diff --git a/src/app/sessions/show.component.spec.ts b/src/app/sessions/show.component.spec.ts index b2d3b1f94..711cd8143 100644 --- a/src/app/sessions/show.component.spec.ts +++ b/src/app/sessions/show.component.spec.ts @@ -9,6 +9,7 @@ import { FiltersService } from '@services/filters.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; import { SessionsGrpcService } from './services/sessions-grpc.service'; import { SessionsInspectionService } from './services/sessions-inspection.service'; @@ -27,6 +28,12 @@ describe('AppShowComponent', () => { generateSharableURL: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Sessions:GetSession', 'Sessions:CancelSession', 'Sessions:PauseSession', 'Sessions:ResumeSession', 'Sessions:CloseSession', 'Sessions:PurgeSession', 'Sessions:DeleteSession', 'Sessions:CreateSession', 'Sessions:StopSubmission'] + } + }; + const paramId = 'paramId-12345'; const mockActivatedRoute = { params: new BehaviorSubject({ @@ -99,6 +106,7 @@ describe('AppShowComponent', () => { { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: SessionsGrpcService, useValue: mockSessionsGrpcService }, { provide: Router, useValue: mockRouter }, + { provide: UserService, useValue: mockUserService }, SessionsInspectionService, TasksInspectionService, ] diff --git a/src/app/sessions/show.component.ts b/src/app/sessions/show.component.ts index 5eae40d6d..0a8d77567 100644 --- a/src/app/sessions/show.component.ts +++ b/src/app/sessions/show.component.ts @@ -23,6 +23,7 @@ import { StorageService } from '@services/storage.service'; import { TableStorageService } from '@services/table-storage.service'; import { TableURLService } from '@services/table-url.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { UtilsService } from '@services/utils.service'; import { Subject, map, switchMap } from 'rxjs'; import { SessionsFiltersService } from './services/sessions-filters.service'; @@ -78,6 +79,7 @@ export class ShowComponent extends AppShowComponent[]; arrays: Field[]; @@ -136,11 +178,11 @@ export class ShowComponent extends AppShowComponent { taskNotEnded: jest.fn((s: TaskStatus) => s !== TaskStatus.TASK_STATUS_COMPLETED) }; + const mockUserService = { + user: { + permissions: ['Tasks:CancelTask', 'Results:GetResult'] + } + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -87,7 +94,8 @@ describe('TasksTableComponent', () => { { provide: NotificationService, useValue: mockNotificationService }, { provide: Clipboard, useValue: mockClipBoard }, { provide: Router, useValue: mockRouter }, - { provide: TasksDataService, useValue: mockTasksDataService } + { provide: TasksDataService, useValue: mockTasksDataService }, + { provide: UserService, useValue: mockUserService } ] }).inject(TasksTableComponent); diff --git a/src/app/tasks/components/table.component.ts b/src/app/tasks/components/table.component.ts index 1b93ac0fa..27cdf6e9f 100644 --- a/src/app/tasks/components/table.component.ts +++ b/src/app/tasks/components/table.component.ts @@ -8,6 +8,7 @@ import { ArmonikData, TaskData } from '@app/types/data'; import { StatusService } from '@app/types/status'; import { ActionTable } from '@app/types/table'; import { TableComponent } from '@components/table/table.component'; +import { UserService } from '@services/user.service'; import { Subject } from 'rxjs'; import TasksDataService from '../services/tasks-data.service'; import { TasksStatusesService } from '../services/tasks-statuses.service'; @@ -53,6 +54,7 @@ export class TasksTableComponent extends AbstractTableComponent Tasks - +@if (hasListTasksPermission) { + - @@ -66,4 +71,4 @@ (optionsUpdate)="onOptionsChange()" (selectionChange)="onSelectionChange($event)" /> - \ No newline at end of file +} diff --git a/src/app/tasks/index.component.spec.ts b/src/app/tasks/index.component.spec.ts index 268efc83d..3947dc178 100644 --- a/src/app/tasks/index.component.spec.ts +++ b/src/app/tasks/index.component.spec.ts @@ -12,6 +12,7 @@ import { AutoRefreshService } from '@services/auto-refresh.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { of } from 'rxjs'; import { IndexComponent } from './index.component'; import TasksDataService from './services/tasks-data.service'; @@ -148,6 +149,12 @@ describe('Tasks Index Component', () => { warning: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Tasks:ListTasks', 'Tasks:CancelTasks'] + } + }; + const mockTasksDataService = { data: [], total: 0, @@ -174,6 +181,7 @@ describe('Tasks Index Component', () => { { provide: TasksFiltersService, useValue: mockTaskFiltersService }, { provide: ShareUrlService, useValue: mockShareUrlService }, { provide: NotificationService, useValue: mockNotificationService }, + { provide: UserService, useValue: mockUserService }, ] }).inject(IndexComponent); component.ngOnInit(); diff --git a/src/app/tasks/index.component.ts b/src/app/tasks/index.component.ts index 031924843..62a726d3d 100644 --- a/src/app/tasks/index.component.ts +++ b/src/app/tasks/index.component.ts @@ -24,6 +24,7 @@ import { ShareUrlService } from '@services/share-url.service'; import { TableStorageService } from '@services/table-storage.service'; import { TableURLService } from '@services/table-url.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { UtilsService } from '@services/utils.service'; import { ManageViewInLogsDialogComponent } from './components/manage-view-in-logs-dialog.component'; import { TasksTableComponent } from './components/table.component'; @@ -81,6 +82,7 @@ export class IndexComponent extends TableHandlerCustomValues { restoreViewInLogs: jest.fn((): { serviceIcon: string, serviceName: string, urlTemplate: string } | null => storedViewInLogs) }; + const mockUserService = { + user: { + permissions: ['Tasks:ListTasks', 'Tasks:GetTask'] + } + }; + beforeEach(() => { service = TestBed.configureTestingModule({ providers: [ TasksIndexService, DefaultConfigService, - { provide: TableService, useValue: mockTableService } + { provide: TableService, useValue: mockTableService }, + { provide: UserService, useValue: mockUserService } ] }).inject(TasksIndexService); }); diff --git a/src/app/tasks/services/tasks-index.service.ts b/src/app/tasks/services/tasks-index.service.ts index 14d013866..4838ca0af 100644 --- a/src/app/tasks/services/tasks-index.service.ts +++ b/src/app/tasks/services/tasks-index.service.ts @@ -4,12 +4,14 @@ import { CustomColumn } from '@app/types/data'; import { IndexServiceCustomInterface } from '@app/types/services/indexService'; import { DefaultConfigService } from '@services/default-config.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { TaskOptions, TaskSummary, TaskSummaryColumnKey, TaskSummaryListOptions } from '../types'; @Injectable() export class TasksIndexService implements IndexServiceCustomInterface { defaultConfigService = inject(DefaultConfigService); tableService = inject(TableService); + userService = inject(UserService); readonly defaultColumns: TaskSummaryColumnKey[] = this.defaultConfigService.defaultTasks.columns; readonly defaultLockColumns: boolean = this.defaultConfigService.defaultTasks.lockColumns; @@ -17,7 +19,7 @@ export class TasksIndexService implements IndexServiceCustomInterface[] = [ + private readonly _allTableColumns: TableColumn[] = [ { name: $localize`Task ID`, key: 'id', @@ -240,6 +242,25 @@ export class TasksIndexService implements IndexServiceCustomInterface[] { + const permissions = this.userService.user?.permissions ?? []; + const hasDetailedPermission = permissions.includes('Tasks:ListTasksDetailed'); + + if (hasDetailedPermission) { + return this._allTableColumns; + } + + return this._allTableColumns.filter(column => !this.detailedColumns.includes(column.key as string)); + } + customField(column: TaskSummaryColumnKey) { return column.replace('options.options.', ''); } diff --git a/src/app/tasks/show.component.html b/src/app/tasks/show.component.html index 0d7073712..10f36ff62 100644 --- a/src/app/tasks/show.component.html +++ b/src/app/tasks/show.component.html @@ -18,7 +18,11 @@
- diff --git a/src/app/tasks/show.component.spec.ts b/src/app/tasks/show.component.spec.ts index 743e95172..2ce8128a1 100644 --- a/src/app/tasks/show.component.spec.ts +++ b/src/app/tasks/show.component.spec.ts @@ -7,6 +7,7 @@ import { FiltersService } from '@services/filters.service'; import { IconsService } from '@services/icons.service'; import { NotificationService } from '@services/notification.service'; import { ShareUrlService } from '@services/share-url.service'; +import { UserService } from '@services/user.service'; import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; import { TasksGrpcService } from './services/tasks-grpc.service'; import { TasksInspectionService } from './services/tasks-inspection.service'; @@ -25,6 +26,12 @@ describe('AppShowComponent', () => { generateSharableURL: jest.fn(), }; + const mockUserService = { + user: { + permissions: ['Tasks:GetTask', 'Tasks:CancelTasks', 'Results:GetResult'] + } + }; + const paramId = 'paramId-12345'; const mockActivatedRoute = { params: new BehaviorSubject({ @@ -74,6 +81,7 @@ describe('AppShowComponent', () => { { provide: ShareUrlService, useValue: mockShareUrlService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: TasksGrpcService, useValue: mockTasksGrpcService }, + { provide: UserService, useValue: mockUserService }, TasksInspectionService ] }).inject(ShowComponent); diff --git a/src/app/tasks/show.component.ts b/src/app/tasks/show.component.ts index eebcd9fb7..c315832ae 100644 --- a/src/app/tasks/show.component.ts +++ b/src/app/tasks/show.component.ts @@ -19,6 +19,7 @@ import { StorageService } from '@services/storage.service'; import { TableStorageService } from '@services/table-storage.service'; import { TableURLService } from '@services/table-url.service'; import { TableService } from '@services/table.service'; +import { UserService } from '@services/user.service'; import { UtilsService } from '@services/utils.service'; import { TasksFiltersService } from './services/tasks-filters.service'; import { TasksGrpcService } from './services/tasks-grpc.service'; @@ -68,6 +69,7 @@ export class ShowComponent extends AppShowComponent im private readonly tasksStatusesService = inject(StatusService) as TasksStatusesService; private readonly filtersService = inject(FiltersService); + private readonly userService = inject(UserService); private _status: StatusLabelColor | undefined; @@ -83,6 +85,11 @@ export class ShowComponent extends AppShowComponent im return this._status; } + get hasCancelTaskPermission(): boolean { + const permissions = this.userService.user?.permissions ?? []; + return permissions.includes('Tasks:CancelTask'); + } + set status(value: TaskStatus | undefined) { this._status = value ? this.statuses[value] : undefined; } @@ -106,7 +113,7 @@ export class ShowComponent extends AppShowComponent im if (data) { data.parentTaskIds = data.parentTaskIds.filter(taskId => taskId !== data.sessionId); this.createResultQueryParams(); - this.canCancel = !this.tasksStatusesService.taskNotEnded(data.status); + this.canCancel = !this.tasksStatusesService.taskNotEnded(data.status) && this.hasCancelTaskPermission; } }