Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -42,6 +42,7 @@ function initializeAppFactory(userGrpcService: UserGrpcService, userService: Use
throw new Error('No user');
}
userService.user = data.user;
navigationService.refreshSidebar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you have to do this, since it already initialize the sidebar on its construction (when the service is provided for the first time)

}),
catchError((err) => {
console.error(err);
Expand Down Expand Up @@ -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(),
Expand Down
20 changes: 15 additions & 5 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -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 = [
{
Expand All @@ -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',
Expand Down
23 changes: 23 additions & 0 deletions src/app/applications/guards/applications-access.guard.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +8 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you create an abstract class with just an abstract member permission, which would represent in this case "Applications:ListApplications".

We would have in the end:

export class ApplicationsAccessGuard extends AbstractAcessGuard {
  permission = 'Applications:ListApplications';
}

}
7 changes: 6 additions & 1 deletion src/app/applications/routes.ts
Original file line number Diff line number Diff line change
@@ -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]
},
];
18 changes: 11 additions & 7 deletions src/app/components/count-tasks-by-status.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<app-view-tasks-by-status
[defaultQueryParams]="queryParams"
[loading]="loading"
[statusesGroups]="statusesGroups"
[statusesCount]="statusesCount()"
>
</app-view-tasks-by-status>
@if (hasCountPermission) {
<app-view-tasks-by-status
[defaultQueryParams]="queryParams"
[loading]="loading"
[statusesGroups]="statusesGroups"
[statusesCount]="statusesCount()"
>
</app-view-tasks-by-status>
} @else {
<span i18n>No permission to view tasks count</span>
}
11 changes: 10 additions & 1 deletion src/app/components/count-tasks-by-status.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -43,12 +44,20 @@ describe('CountTasksByStatusComponent', () => {
const refresh$ = new Subject<void>();
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide a test for:

  • hasCountPermission
  • The changes you've made in the ngOnInit ?
  • The changes you've made in the initRefresh ?


Expand Down
16 changes: 16 additions & 0 deletions src/app/components/count-tasks-by-status.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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<StatusCount[]> = 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<void>;
Expand Down Expand Up @@ -59,6 +66,10 @@ export class CountTasksByStatusComponent implements OnInit {
}

ngOnInit(): void {
if (!this.hasCountPermission) {
this.loading = false;
return;
}
this.initId();
}

Expand All @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/inspection/byte-array.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
} @else {
<span class="binary-indicator" i18n>- Binary</span>
}
@if (byteLength) {
@if (byteLength && hasDownloadPermission) {
<button mat-icon-button (click)="download()">
<mat-icon [fontIcon]="getIcon('download')" />
</button>
Expand Down
8 changes: 8 additions & 0 deletions src/app/components/inspection/byte-array.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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 += ' ';
Expand All @@ -44,6 +51,7 @@ describe('ByteArrayComponent', () => {
{ provide: IconsService, useValue: mockIconsService },
{ provide: Clipboard, useValue: mockClipboard },
{ provide: NotificationService, useValue: mockNotificationService },
{ provide: UserService, useValue: mockUserService },
]
}).inject(ByteArrayComponent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide test for hasDownloadPermission


Expand Down
7 changes: 7 additions & 0 deletions src/app/components/inspection/byte-array.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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');
}
Comment on lines +96 to +99

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not generic enough. if we have another download action, we will have to add it there, and it will not be easy to do it in every component.
If you look inside the inspection-object component, you will see that we use an object called field. I think you should update the type Field by adding a permission (which could be a string or an array of string containing "result:downloadResultdata").
Then you could add the input permissions to the component, pass it via inspection-object, and replace the string with this input.
Finally, you will just have to add the permission inside the various ...-inspection.service.ts.

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<mat-icon [fontIcon]="getIcon('copy')" />
</button>
}
@if (decodedData || byteLength) {
@if ((decodedData || byteLength) && hasDownloadPermission) {
<button mat-icon-button (click)="download()" [matTooltip]="downloadTip + label">
<mat-icon [fontIcon]="getIcon('download')" />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -25,6 +26,12 @@ describe('ByteArrayComponent', () => {
success: jest.fn(),
};

const mockUserService = {
user: {
permissions: ['Results:DownloadResultData']
}
};


let dataContent = '';
for(let i = 0; i !== 128; i++) {
Expand All @@ -45,6 +52,7 @@ describe('ByteArrayComponent', () => {
{ provide: IconsService, useValue: mockIconsService },
{ provide: Clipboard, useValue: mockClipboard },
{ provide: NotificationService, useValue: mockNotificationService },
{ provide: UserService, useValue: mockUserService },
]
}).inject(ByteArrayComponent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide test for hasDownloadPermission


Expand Down
7 changes: 7 additions & 0 deletions src/app/components/table/cells/byte-array-cell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda like the byte-array component, this is not generic enough. This time, you can check in the table-cell component that we provide various objects, including an object called column, of type TableColumn.

You will need to update the type by adding a permissions value (string or string[]). Then, you need to add a new input to this component, and use the provided value to check the permission.

}
}
28 changes: 19 additions & 9 deletions src/app/dashboard/components/add-line-dialog.component.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
<h2 mat-dialog-title i18n="Dialog title">Add a line</h2>

<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
@if (hasAvailableTypes) {
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<mat-dialog-content>
<mat-form-field appearance="outline">
<mat-label for="name" i18n="Name of the statuses group"> Name </mat-label>
<input matInput id="name" type="text" formControlName="name" i18n-placeholder="Placeholder" placeholder="Name of your line" required i18n="Input error">
</mat-form-field>
<app-autocomplete [options]="types" [value]="type" [label]="typeLabel" (valueChange)="onTypeChange($event)" />
</mat-dialog-content>

<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()" type="button" i18n="Dialog action"> Cancel </button>
<button mat-flat-button type="submit" color="primary" [disabled]="!validType" i18n="Dialog action"> Confirm </button>
</mat-dialog-actions>
</form>
} @else {
<mat-dialog-content>
<mat-form-field appearance="outline">
<mat-label for="name" i18n="Name of the statuses group"> Name </mat-label>
<input matInput id="name" type="text" formControlName="name" i18n-placeholder="Placeholder" placeholder="Name of your line" required i18n="Input error">
</mat-form-field>
<app-autocomplete [options]="types" [value]="type" [label]="typeLabel" (valueChange)="onTypeChange($event)" />
<p i18n="No permissions message">You don't have permissions to add any type of line.</p>
</mat-dialog-content>

<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()" type="button" i18n="Dialog action"> Cancel </button>
<button mat-flat-button type="submit" color="primary" [disabled]="!validType" i18n="Dialog action"> Confirm </button>
<button mat-button (click)="onCancel()" type="button" i18n="Dialog action"> Close </button>
</mat-dialog-actions>
</form>
}
Loading
Loading