mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-04-25 21:40:03 +00:00
ACE management
This commit is contained in:
parent
6b5b784658
commit
4870e58977
@ -69,6 +69,7 @@ import { GroupResolver } from "./resolvers/group.resolver";
|
|||||||
import { GroupRoleResolver } from "./resolvers/group-role.resolver";
|
import { GroupRoleResolver } from "./resolvers/group-role.resolver";
|
||||||
import { RoleDetailComponent } from "./components/role-management/role-detail/role-detail.component";
|
import { RoleDetailComponent } from "./components/role-management/role-detail/role-detail.component";
|
||||||
import { RoleDetailResolver } from "./resolvers/role-detail.resolver";
|
import { RoleDetailResolver } from "./resolvers/role-detail.resolver";
|
||||||
|
import {AceManagementComponent} from "@components/ace-management/ace-management.component";
|
||||||
import {ResourcePoolsManagementComponent} from "@components/resource-pools-management/resource-pools-management.component";
|
import {ResourcePoolsManagementComponent} from "@components/resource-pools-management/resource-pools-management.component";
|
||||||
import {ResourcePoolDetailsComponent} from "@components/resource-pool-details/resource-pool-details.component";
|
import {ResourcePoolDetailsComponent} from "@components/resource-pool-details/resource-pool-details.component";
|
||||||
import {ResourcePoolsResolver} from "@resolvers/resource-pools.resolver";
|
import {ResourcePoolsResolver} from "@resolvers/resource-pools.resolver";
|
||||||
@ -247,6 +248,10 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "resourcePools",
|
path: "resourcePools",
|
||||||
component: ResourcePoolsManagementComponent
|
component: ResourcePoolsManagementComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aces',
|
||||||
|
component: AceManagementComponent
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -309,6 +309,14 @@ import { ExportPortableProjectComponent } from './components/export-portable-pro
|
|||||||
import { NodesMenuConfirmationDialogComponent } from './components/project-map/nodes-menu/nodes-menu-confirmation-dialog/nodes-menu-confirmation-dialog.component';
|
import { NodesMenuConfirmationDialogComponent } from './components/project-map/nodes-menu/nodes-menu-confirmation-dialog/nodes-menu-confirmation-dialog.component';
|
||||||
import { ConfirmationDeleteAllProjectsComponent } from './components/projects/confirmation-delete-all-projects/confirmation-delete-all-projects.component';
|
import { ConfirmationDeleteAllProjectsComponent } from './components/projects/confirmation-delete-all-projects/confirmation-delete-all-projects.component';
|
||||||
import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component';
|
import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component';
|
||||||
|
import {AceManagementComponent} from "@components/ace-management/ace-management.component";
|
||||||
|
import { AddAceDialogComponent } from './components/ace-management/add-ace-dialog/add-ace-dialog.component';
|
||||||
|
import { AutocompleteComponent } from './components/ace-management/add-ace-dialog/autocomplete/autocomplete.component';
|
||||||
|
import { DeleteAceDialogComponent } from './components/ace-management/delete-ace-dialog/delete-ace-dialog.component';
|
||||||
|
import { AceFilterPipe } from './filters/ace-filter.pipe';
|
||||||
|
import {CdkAccordionModule} from "@angular/cdk/accordion";
|
||||||
|
import {CdkTreeModule} from "@angular/cdk/tree";
|
||||||
|
|
||||||
import { PrivilegeComponent } from './components/role-management/role-detail/privilege/privilege.component';
|
import { PrivilegeComponent } from './components/role-management/role-detail/privilege/privilege.component';
|
||||||
import { GroupPrivilegesPipe } from './components/role-management/role-detail/privilege/group-privileges.pipe';
|
import { GroupPrivilegesPipe } from './components/role-management/role-detail/privilege/group-privileges.pipe';
|
||||||
import { ResourcePoolsManagementComponent } from './components/resource-pools-management/resource-pools-management.component';
|
import { ResourcePoolsManagementComponent } from './components/resource-pools-management/resource-pools-management.component';
|
||||||
@ -539,6 +547,11 @@ import { DeleteResourceConfirmationDialogComponent } from './components/resource
|
|||||||
NodesMenuConfirmationDialogComponent,
|
NodesMenuConfirmationDialogComponent,
|
||||||
ConfirmationDeleteAllProjectsComponent,
|
ConfirmationDeleteAllProjectsComponent,
|
||||||
ProjectMapLockConfirmationDialogComponent,
|
ProjectMapLockConfirmationDialogComponent,
|
||||||
|
AceManagementComponent,
|
||||||
|
AddAceDialogComponent,
|
||||||
|
AutocompleteComponent,
|
||||||
|
DeleteAceDialogComponent,
|
||||||
|
AceFilterPipe,
|
||||||
PrivilegeComponent,
|
PrivilegeComponent,
|
||||||
GroupPrivilegesPipe,
|
GroupPrivilegesPipe,
|
||||||
ResourcePoolsManagementComponent,
|
ResourcePoolsManagementComponent,
|
||||||
@ -572,6 +585,8 @@ import { DeleteResourceConfirmationDialogComponent } from './components/resource
|
|||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
|
CdkAccordionModule,
|
||||||
|
CdkTreeModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
<div class="content" *ngIf="isReady; else loading">
|
||||||
|
<div class="default-header">
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="col">ACEs management</h1>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="deleteMultiple()" class="add-ace-button" [disabled]="selection.selected.length == 0">
|
||||||
|
Delete selected ACEs
|
||||||
|
</button>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="addACE()" class="add-ace-button">
|
||||||
|
Add ACE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<mat-form-field class="full-width">
|
||||||
|
<input matInput placeholder="Search by path, user/group or role" [(ngModel)]="searchText"
|
||||||
|
[ngModelOptions]="{ standalone: true }"/>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="default-content">
|
||||||
|
<table mat-table [dataSource]="dataSource | aceFilter: searchText:endpoints " class="mat-elevation-z8" matSort #acesSort="matSort">
|
||||||
|
|
||||||
|
<ng-container matColumnDef="select" >
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="small-col">
|
||||||
|
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row" class="small-col">
|
||||||
|
<mat-checkbox (click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? selection.toggle(row) : null"
|
||||||
|
[checked]="selection.isSelected(row)">
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="path">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Path</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{getNameByUuidFromEndpoint(element.path)}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="user/group">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>User/Group</th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<div *ngIf="element.ace_type === 'user' else groupId">{{getNameByUuidFromEndpoint(element.user_id)}}</div>
|
||||||
|
<ng-template #groupId>{{getNameByUuidFromEndpoint(element.group_id)}}</ng-template>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="role">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Role</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{getNameByUuidFromEndpoint(element.role_id)}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="propagate">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Propagate</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.propagate}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="allowed">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Allowed</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.allowed}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="created_at">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Created</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.created_at}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="updated_at">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last update</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.updated_at}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="delete">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> </th>
|
||||||
|
<td mat-cell *matCellDef="let element"><button mat-button (click)="onDelete(element)"><mat-icon>delete</mat-icon></button></td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<mat-paginator #acesPaginator="matPaginator"
|
||||||
|
[pageSizeOptions]="[5, 10, 20]"
|
||||||
|
showFirstLastButtons
|
||||||
|
aria-label="Select page">
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #loading>
|
||||||
|
<div>
|
||||||
|
<mat-spinner class="loader"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -0,0 +1,26 @@
|
|||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 940px;
|
||||||
|
margin-left: -470px;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-ace-button {
|
||||||
|
height: 40px;
|
||||||
|
width: 160px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
margin: auto;
|
||||||
|
height: 175px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 175px;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AceManagementComponent } from './ace-management.component';
|
||||||
|
|
||||||
|
describe('AceManagementComponent', () => {
|
||||||
|
let component: AceManagementComponent;
|
||||||
|
let fixture: ComponentFixture<AceManagementComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ AceManagementComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AceManagementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
176
src/app/components/ace-management/ace-management.component.ts
Normal file
176
src/app/components/ace-management/ace-management.component.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2023 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component, OnInit, QueryList, ViewChildren} from '@angular/core';
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {SelectionModel} from "@angular/cdk/collections";
|
||||||
|
import {Group} from "@models/groups/group";
|
||||||
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
|
import {ACE} from "@models/api/ACE";
|
||||||
|
import {ActivatedRoute} from "@angular/router";
|
||||||
|
import {ControllerService} from "@services/controller.service";
|
||||||
|
import {ToasterService} from "@services/toaster.service";
|
||||||
|
import {GroupService} from "@services/group.service";
|
||||||
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
|
import {AclService} from "@services/acl.service";
|
||||||
|
import {MatPaginator} from "@angular/material/paginator";
|
||||||
|
import {MatSort} from "@angular/material/sort";
|
||||||
|
import {AddUserDialogComponent} from "@components/user-management/add-user-dialog/add-user-dialog.component";
|
||||||
|
import {AddAceDialogComponent} from "@components/ace-management/add-ace-dialog/add-ace-dialog.component";
|
||||||
|
import {DeleteUserDialogComponent} from "@components/user-management/delete-user-dialog/delete-user-dialog.component";
|
||||||
|
import {DeleteAceDialogComponent} from "@components/ace-management/delete-ace-dialog/delete-ace-dialog.component";
|
||||||
|
import {User} from "@models/users/user";
|
||||||
|
import {Endpoint} from "@models/api/endpoint";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-ace-management',
|
||||||
|
templateUrl: './ace-management.component.html',
|
||||||
|
styleUrls: ['./ace-management.component.scss']
|
||||||
|
})
|
||||||
|
export class AceManagementComponent implements OnInit {
|
||||||
|
|
||||||
|
|
||||||
|
@ViewChildren('acesPaginator') acesPaginator: QueryList<MatPaginator>;
|
||||||
|
@ViewChildren('acesSort') acesSort: QueryList<MatSort>;
|
||||||
|
controller: Controller;
|
||||||
|
public displayedColumns = ['select', 'path', 'user/group', 'role', 'propagate', 'allowed', 'updated_at', 'delete'];
|
||||||
|
selection = new SelectionModel<ACE>(true, []);
|
||||||
|
aces: ACE[];
|
||||||
|
dataSource = new MatTableDataSource<ACE>();
|
||||||
|
isReady = false;
|
||||||
|
searchText = '';
|
||||||
|
endpoints: Endpoint[];
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private controllerService: ControllerService,
|
||||||
|
private toasterService: ToasterService,
|
||||||
|
public aclService: AclService,
|
||||||
|
public dialog: MatDialog) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const controllerId = this.route.parent.snapshot.paramMap.get('controller_id');
|
||||||
|
this.controllerService.get(+controllerId).then((controller: Controller) => {
|
||||||
|
this.controller = controller;
|
||||||
|
this.aclService.getEndpoints(this.controller)
|
||||||
|
.subscribe((endpoints: Endpoint[]) => {
|
||||||
|
this.endpoints = endpoints
|
||||||
|
this.refresh();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.acesPaginator.changes.subscribe((comps: QueryList <MatPaginator>) =>
|
||||||
|
{
|
||||||
|
this.dataSource.paginator = comps.first;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.acesSort.changes.subscribe((comps: QueryList<MatSort>) => {
|
||||||
|
this.dataSource.sort = comps.first;
|
||||||
|
})
|
||||||
|
|
||||||
|
this.dataSource.sortingDataAccessor = (item, property) => {
|
||||||
|
switch (property) {
|
||||||
|
case 'path':
|
||||||
|
case 'user/group':
|
||||||
|
case 'role':
|
||||||
|
return item[property] ? item[property].toLowerCase() : '';
|
||||||
|
default:
|
||||||
|
return item[property];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.aclService.list(this.controller).subscribe((aces: ACE[]) => {
|
||||||
|
this.isReady = true;
|
||||||
|
this.aces = aces
|
||||||
|
this.dataSource.data = aces;
|
||||||
|
this.selection.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addACE() {
|
||||||
|
const dialogRef = this.dialog.open(AddAceDialogComponent, {
|
||||||
|
width: '1200px',
|
||||||
|
height: '500px',
|
||||||
|
autoFocus: false,
|
||||||
|
disableClose: true,
|
||||||
|
data: {endpoints: this.endpoints}
|
||||||
|
});
|
||||||
|
let instance = dialogRef.componentInstance;
|
||||||
|
instance.controller = this.controller;
|
||||||
|
dialogRef.afterClosed().subscribe(() => this.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onDelete(ace: ACE) {
|
||||||
|
this.dialog
|
||||||
|
.open(DeleteAceDialogComponent, {width: '500px', data: {aces: [ace]}})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((isDeletedConfirm) => {
|
||||||
|
if (isDeletedConfirm) {
|
||||||
|
this.aclService.delete(this.controller, ace.ace_id)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.refresh()
|
||||||
|
}, (error) => {
|
||||||
|
this.toasterService.error(`An error occur while trying to delete ace ${ace.ace_id}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSelected() {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
const numRows = this.aces.length;
|
||||||
|
return numSelected === numRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
masterToggle() {
|
||||||
|
this.isAllSelected() ?
|
||||||
|
this.selection.clear() :
|
||||||
|
this.aces.forEach(row => this.selection.select(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMultiple() {
|
||||||
|
this.dialog
|
||||||
|
.open(DeleteAceDialogComponent, {width: '500px', data: {aces: this.selection.selected}})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((isDeletedConfirm) => {
|
||||||
|
if (isDeletedConfirm) {
|
||||||
|
this.selection.selected.forEach((ace: ACE) => {
|
||||||
|
this.aclService.delete(this.controller, ace.ace_id)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.refresh()
|
||||||
|
}, (error) => {
|
||||||
|
this.toasterService.error(`An error occur while trying to delete ace ${ace.ace_id}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
this.selection.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getNameByUuidFromEndpoint(uuid: string): string {
|
||||||
|
if (this.endpoints) {
|
||||||
|
const elt = this.endpoints.filter((endpoint: Endpoint) => endpoint.endpoint.includes(uuid))
|
||||||
|
if (elt.length >= 1) {
|
||||||
|
return elt[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import {EndpointTreeAdapter} from "@components/ace-management/add-ace-dialog/EndpointTreeAdapter";
|
||||||
|
import {Endpoint, RessourceType} from "@models/api/endpoint";
|
||||||
|
|
||||||
|
const endpoint1: Endpoint = {
|
||||||
|
endpoint: "/",
|
||||||
|
endpoint_type: RessourceType.image,
|
||||||
|
name: "Root"
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint2: Endpoint = {
|
||||||
|
endpoint: "/projects",
|
||||||
|
endpoint_type: RessourceType.project,
|
||||||
|
name: "All projects"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint3: Endpoint = {
|
||||||
|
endpoint: "/images",
|
||||||
|
endpoint_type: RessourceType.image,
|
||||||
|
name: "All images"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint4: Endpoint = {
|
||||||
|
endpoint: "/projects/blabla",
|
||||||
|
endpoint_type: RessourceType.project,
|
||||||
|
name: "Project blabla"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint5 : Endpoint = {
|
||||||
|
endpoint: "/projects/blabla/nodes",
|
||||||
|
endpoint_type: RessourceType.node,
|
||||||
|
name: "All nodes for project blabla"
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint6 : Endpoint = {
|
||||||
|
endpoint: "/images/blabla",
|
||||||
|
endpoint_type: RessourceType.image,
|
||||||
|
name: "Image blabla"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let endpoints: Endpoint[] = [endpoint1, endpoint2, endpoint3, endpoint4, endpoint5, endpoint6];
|
||||||
|
|
||||||
|
describe('EndpointTreeAdapter', () => {
|
||||||
|
|
||||||
|
it('Should build endpointTree', () => {
|
||||||
|
|
||||||
|
const adapter = new EndpointTreeAdapter(endpoints);
|
||||||
|
const tree = adapter.buildTreeFromEndpoints()
|
||||||
|
expect(tree.length).toEqual(1);
|
||||||
|
expect(tree[0].children.length).toEqual(2);
|
||||||
|
|
||||||
|
const projectEndpoint = tree[0].children[0];
|
||||||
|
|
||||||
|
expect(projectEndpoint.children.length).toEqual(1);
|
||||||
|
expect(projectEndpoint.children[0].children.length).toEqual(1);
|
||||||
|
|
||||||
|
const imageEndpoint = tree[0].children[0];
|
||||||
|
expect(imageEndpoint.children.length).toEqual(1)
|
||||||
|
expect(imageEndpoint.children[0].children.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should build empty tree', () => {
|
||||||
|
const adapter = new EndpointTreeAdapter([]);
|
||||||
|
const tree = adapter.buildTreeFromEndpoints()
|
||||||
|
|
||||||
|
expect(tree.length).toEqual(0);
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,59 @@
|
|||||||
|
import {Endpoint, RessourceType} from "../../../models/api/endpoint";
|
||||||
|
|
||||||
|
export interface EndpointNode {
|
||||||
|
endpoint: string;
|
||||||
|
name: string;
|
||||||
|
endpoint_type: RessourceType;
|
||||||
|
depth: number;
|
||||||
|
splitEndp: string[];
|
||||||
|
parent?: string[];
|
||||||
|
children?: EndpointNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EndpointTreeAdapter {
|
||||||
|
private endpoints: Endpoint[]
|
||||||
|
|
||||||
|
constructor(endpoints: Endpoint[]) {
|
||||||
|
this.endpoints = endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTreeFromEndpoints(): EndpointNode[] {
|
||||||
|
const parentNode: EndpointNode[] = []
|
||||||
|
let nodes = []
|
||||||
|
this.endpoints.forEach((endp: Endpoint) => {
|
||||||
|
const node = this.extractParent(endp)
|
||||||
|
nodes.push(node)
|
||||||
|
})
|
||||||
|
|
||||||
|
nodes.forEach((node: EndpointNode) => {
|
||||||
|
if(node.depth > 0) {
|
||||||
|
const parent = nodes.filter((n: EndpointNode) => n.splitEndp.join('/') == node.splitEndp.slice(0, node.depth-1).join('/'))[0]
|
||||||
|
parent.children.push(node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
parentNode.push(nodes.find((node: EndpointNode) => node.depth === 0))
|
||||||
|
|
||||||
|
return parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractParent(endp: Endpoint): EndpointNode {
|
||||||
|
|
||||||
|
let splitEndp = endp.endpoint.split('/');
|
||||||
|
splitEndp = splitEndp.filter((value: string) => value !== '' && value !== 'access')
|
||||||
|
let parent = [];
|
||||||
|
if (splitEndp.length > 0) {
|
||||||
|
parent = splitEndp.slice(0,splitEndp.length - 1)
|
||||||
|
}
|
||||||
|
const node: EndpointNode = {
|
||||||
|
children: [],
|
||||||
|
depth: splitEndp.length,
|
||||||
|
splitEndp: splitEndp,
|
||||||
|
endpoint: endp.endpoint,
|
||||||
|
endpoint_type: endp.endpoint_type,
|
||||||
|
name: endp.name,
|
||||||
|
parent: parent
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
<h1 mat-dialog-title>Create new ACE</h1>
|
||||||
|
<form [formGroup]="addAceForm" class="input-field d-flex" style="height: 380px">
|
||||||
|
<div style="width: 75%; float: left; overflow-y: auto" class="d-inline-block">
|
||||||
|
<cdk-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||||
|
<!-- This is the tree node template for leaf nodes -->
|
||||||
|
<cdk-nested-tree-node *cdkTreeNodeDef="let node" class="example-tree-node">
|
||||||
|
<!-- use a disabled button to provide padding for tree leaf -->
|
||||||
|
<div [class.selected]="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint">
|
||||||
|
<button mat-icon-button disabled></button>
|
||||||
|
{{node.name}}
|
||||||
|
<button mat-icon-button (click)="endpointSelection(node)">
|
||||||
|
<div *ngIf="!selectedEndpoint || node.endpoint !== selectedEndpoint.endpoint">
|
||||||
|
<mat-icon class="align-middle" style="color: #696969">panorama_fish_eye</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint" >
|
||||||
|
<mat-icon class="align-middle">check_circle</mat-icon>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</cdk-nested-tree-node>
|
||||||
|
<!-- This is the tree node template for expandable nodes -->
|
||||||
|
<cdk-nested-tree-node *cdkTreeNodeDef="let node; when: hasChild" class="example-tree-node" >
|
||||||
|
<div [class.selected]="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint">
|
||||||
|
<button mat-icon-button [attr.aria-label]="'Toggle ' + node.name" cdkTreeNodeToggle>
|
||||||
|
<mat-icon class="mat-icon-rtl-mirror">
|
||||||
|
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
{{node.name}}
|
||||||
|
<button mat-icon-button (click)="endpointSelection(node)">
|
||||||
|
<div *ngIf="!selectedEndpoint || node.endpoint !== selectedEndpoint.endpoint">
|
||||||
|
<mat-icon class="align-middle" style="color: #696969">panorama_fish_eye</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint">
|
||||||
|
<mat-icon class="align-middle">check_circle</mat-icon>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [class.example-tree-invisible]="!treeControl.isExpanded(node)">
|
||||||
|
<ng-container cdkTreeNodeOutlet></ng-container>
|
||||||
|
</div>
|
||||||
|
</cdk-nested-tree-node>
|
||||||
|
</cdk-tree>
|
||||||
|
</div>
|
||||||
|
<mat-divider [vertical]="true" style="height: auto;"></mat-divider>
|
||||||
|
|
||||||
|
<div class="form-div d-inline-block h-100">
|
||||||
|
<div>
|
||||||
|
<div class="typeSelect">
|
||||||
|
<mat-select placeholder="User/Group" formControlName="type">
|
||||||
|
<mat-option *ngFor="let t of types" [value]="t">{{t.charAt(0).toUpperCase() + t.slice(1)}}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-autocomplete *ngIf="form.type.value === 'user'"
|
||||||
|
[data]="users"
|
||||||
|
[eltType]="'Users'"
|
||||||
|
[displayFn]="displayFnUser"
|
||||||
|
[filterFn]="_filterUser"
|
||||||
|
(onSelection)="userSelection($event)">
|
||||||
|
</app-autocomplete>
|
||||||
|
|
||||||
|
<app-autocomplete *ngIf="form.type.value === 'group'"
|
||||||
|
[data]="groups"
|
||||||
|
[eltType]="'Groups'"
|
||||||
|
[displayFn]="displayFn"
|
||||||
|
[filterFn]="_filter"
|
||||||
|
(onSelection)="groupSelection($event)">
|
||||||
|
</app-autocomplete>
|
||||||
|
|
||||||
|
<app-autocomplete
|
||||||
|
[data]="roles"
|
||||||
|
[eltType]="'Roles'"
|
||||||
|
[displayFn]="displayFn"
|
||||||
|
[filterFn]="_filter"
|
||||||
|
(onSelection)="roleSelection($event)">
|
||||||
|
</app-autocomplete>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div class="d-inline-block" style="float:left;">
|
||||||
|
<mat-checkbox formControlName="propagate" >Propagate</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<mat-divider [vertical]="true" style="height: auto;"></mat-divider>
|
||||||
|
<div class="d-inline-block" style="float: right">
|
||||||
|
<button *ngIf="allowed" class="allow "
|
||||||
|
mat-button
|
||||||
|
(click)="changeAllowed()">ALLOWED
|
||||||
|
</button>
|
||||||
|
<button *ngIf="!allowed" class="deny"
|
||||||
|
mat-button
|
||||||
|
(click)="changeAllowed()">DENY
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onCancelClick()" color="accent">Cancel</button>
|
||||||
|
<button mat-button (click)="onAddClick()" tabindex="2" mat-raised-button color="primary"
|
||||||
|
[disabled]="!addAceForm.valid">
|
||||||
|
Add ACE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
@ -0,0 +1,71 @@
|
|||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.height-100 {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-div {
|
||||||
|
float: right;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allow {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deny {
|
||||||
|
background-color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeSelect {
|
||||||
|
height: 25px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupList {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1 1 auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups {
|
||||||
|
display: flex;
|
||||||
|
height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-tree-invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-tree ul,
|
||||||
|
.example-tree li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
.example-tree-node {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-tree-node .example-tree-node {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-div {
|
||||||
|
position: relative;
|
||||||
|
width: 25%;
|
||||||
|
float:right;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
color: green
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AddAceDialogComponent } from './add-ace-dialog.component';
|
||||||
|
|
||||||
|
describe('AddAceDialogComponent', () => {
|
||||||
|
let component: AddAceDialogComponent;
|
||||||
|
let fixture: ComponentFixture<AddAceDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ AddAceDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AddAceDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2023 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||||
|
import {UserService} from "@services/user.service";
|
||||||
|
import {ToasterService} from "@services/toaster.service";
|
||||||
|
import {AclService} from "@services/acl.service";
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {Endpoint, RessourceType} from "@models/api/endpoint";
|
||||||
|
import {UntypedFormControl, UntypedFormGroup} from "@angular/forms";
|
||||||
|
import {ACE, AceType} from "@models/api/ACE";
|
||||||
|
import {Group} from "@models/groups/group";
|
||||||
|
import {GroupService} from "@services/group.service";
|
||||||
|
import {User} from "@models/users/user";
|
||||||
|
import {Role} from "@models/api/role";
|
||||||
|
import {RoleService} from "@services/role.service";
|
||||||
|
import {NestedTreeControl} from "@angular/cdk/tree";
|
||||||
|
import {ArrayDataSource} from "@angular/cdk/collections";
|
||||||
|
import {EndpointNode, EndpointTreeAdapter} from "@components/ace-management/add-ace-dialog/EndpointTreeAdapter";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-add-ace-dialog',
|
||||||
|
templateUrl: './add-ace-dialog.component.html',
|
||||||
|
styleUrls: ['./add-ace-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class AddAceDialogComponent implements OnInit {
|
||||||
|
controller: Controller
|
||||||
|
addAceForm: UntypedFormGroup
|
||||||
|
allowed: boolean = true
|
||||||
|
types = Object.values(AceType);
|
||||||
|
|
||||||
|
endpoints: Endpoint[];
|
||||||
|
selectedEndpoint: Endpoint
|
||||||
|
filteredEndpoint: Endpoint[]
|
||||||
|
endpointTypes: string[]
|
||||||
|
|
||||||
|
groups: Group[];
|
||||||
|
selectedGroup: Group;
|
||||||
|
|
||||||
|
users: User[];
|
||||||
|
selectedUser: User;
|
||||||
|
|
||||||
|
roles: Role[];
|
||||||
|
selectedRole: Role;
|
||||||
|
|
||||||
|
TREE_DATA: EndpointNode[] = [];
|
||||||
|
treeControl = new NestedTreeControl<EndpointNode>(node => node.children);
|
||||||
|
treeDataSource: ArrayDataSource<EndpointNode> ;
|
||||||
|
|
||||||
|
constructor(public dialogRef: MatDialogRef<AddAceDialogComponent>,
|
||||||
|
public aclService: AclService,
|
||||||
|
public userService: UserService,
|
||||||
|
private groupService: GroupService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private toasterService: ToasterService,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { endpoints: Endpoint[] }) {
|
||||||
|
this.endpoints = data.endpoints
|
||||||
|
const treeAdapter = new EndpointTreeAdapter(this.endpoints)
|
||||||
|
const data_tree = treeAdapter.buildTreeFromEndpoints()
|
||||||
|
this.treeDataSource = new ArrayDataSource(data_tree);
|
||||||
|
console.log(data_tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.addAceForm = new UntypedFormGroup({
|
||||||
|
type: new UntypedFormControl(AceType.user),
|
||||||
|
role_id: new UntypedFormControl(),
|
||||||
|
propagate: new UntypedFormControl(true)
|
||||||
|
});
|
||||||
|
this.groupService.getGroups(this.controller)
|
||||||
|
.subscribe((groups: Group[]) => {
|
||||||
|
this.groups = groups;
|
||||||
|
})
|
||||||
|
this.userService.list(this.controller)
|
||||||
|
.subscribe((users: User[]) => {
|
||||||
|
this.users = users;
|
||||||
|
})
|
||||||
|
this.roleService.get(this.controller)
|
||||||
|
.subscribe((roles: Role[]) => {
|
||||||
|
this.roles = roles;
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this.addAceForm.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancelClick() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddClick() {
|
||||||
|
const ACE = {
|
||||||
|
ace_type: this.form.type.value,
|
||||||
|
allowed: this.allowed,
|
||||||
|
group_id: this.form.type.value === AceType.group ? this.selectedGroup.user_group_id : null,
|
||||||
|
path: this.selectedEndpoint.endpoint,
|
||||||
|
propagate: this.form.propagate.value,
|
||||||
|
role_id: this.selectedRole.role_id,
|
||||||
|
user_id: this.form.type.value === AceType.user ? this.selectedUser.user_id : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ACE.path && ACE.role_id && (ACE.user_id || ACE.group_id)) {
|
||||||
|
this.aclService.add(this.controller, ACE)
|
||||||
|
.subscribe((ace: ACE) => {
|
||||||
|
this.toasterService.success(`ACE was added for path ${ACE.path}`);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.toasterService.error(`Cannot create ACE : ${error.error.message}`)
|
||||||
|
})
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeAllowed() {
|
||||||
|
this.allowed = !this.allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
displayFn(value): string {
|
||||||
|
return value && value.name ? value.name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
displayFnUser(value): string {
|
||||||
|
return value && value.full_name && value.username ? value.username.concat(" - ", value.full_name) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
_filter(value: string, data: any): any {
|
||||||
|
if (typeof value === 'string' && data) {
|
||||||
|
const filterValue = value.toLowerCase();
|
||||||
|
|
||||||
|
return data.filter(option => option.name.toLowerCase().includes(filterValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_filterUser(value: string, users: User[]): User[] {
|
||||||
|
if (typeof value === 'string' && users) {
|
||||||
|
const filterValue = value.toLowerCase();
|
||||||
|
|
||||||
|
return users.filter(option => option.full_name.toLowerCase().includes(filterValue) || option.username.toLowerCase().includes(filterValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_filterRole(value: string, roles: Role[]) {
|
||||||
|
if (typeof value === 'string' && roles) {
|
||||||
|
const filterValue = value.toLowerCase();
|
||||||
|
|
||||||
|
return roles.filter(option => option.name.toLowerCase().includes(filterValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userSelection(value: any) {
|
||||||
|
this.selectedUser = value
|
||||||
|
}
|
||||||
|
|
||||||
|
groupSelection(value: any) {
|
||||||
|
this.selectedGroup = value
|
||||||
|
}
|
||||||
|
|
||||||
|
roleSelection(value: any) {
|
||||||
|
this.selectedRole = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointSelection(value: EndpointNode) {
|
||||||
|
const endp: Endpoint = {
|
||||||
|
endpoint: value.endpoint,
|
||||||
|
endpoint_type: value.endpoint_type,
|
||||||
|
name: value.name
|
||||||
|
}
|
||||||
|
this.selectedEndpoint = endp
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChild = (_: number, node: EndpointNode) => !!node.children && node.children.length > 0;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<mat-form-field class="input-field">
|
||||||
|
<mat-label>{{eltType}}</mat-label>
|
||||||
|
<input type="text"
|
||||||
|
matInput
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
[formControl]="autocompleteControl">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)='onSelection.emit($event.option.value)'>
|
||||||
|
<mat-option *ngFor="let elt of filteredData | async" [value]="elt" >
|
||||||
|
{{displayFn(elt)}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
@ -0,0 +1,3 @@
|
|||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AutocompleteComponent } from './autocomplete.component';
|
||||||
|
|
||||||
|
describe('AutocompleteComponent', () => {
|
||||||
|
let component: AutocompleteComponent<any>;
|
||||||
|
let fixture: ComponentFixture<AutocompleteComponent<any>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ AutocompleteComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AutocompleteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
||||||
|
import {Group} from "@models/groups/group";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {UntypedFormControl} from "@angular/forms";
|
||||||
|
import {map, startWith} from "rxjs/operators";
|
||||||
|
import {data} from "autoprefixer";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-autocomplete',
|
||||||
|
templateUrl: './autocomplete.component.html',
|
||||||
|
styleUrls: ['./autocomplete.component.scss']
|
||||||
|
})
|
||||||
|
export class AutocompleteComponent<T> implements OnChanges {
|
||||||
|
|
||||||
|
@Input() data: T[];
|
||||||
|
filteredData: Observable<T[]>;
|
||||||
|
typeName: string
|
||||||
|
autocompleteControl = new UntypedFormControl();
|
||||||
|
|
||||||
|
@Input() eltType: string
|
||||||
|
@Input() displayFn: (value: T) => string
|
||||||
|
@Input() filterFn: (value: string, data: T[]) => T[]
|
||||||
|
@Output() onSelection: EventEmitter<T> = new EventEmitter<T>();
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.filteredData = this.autocompleteControl.valueChanges.pipe(
|
||||||
|
startWith(''),
|
||||||
|
map(value => this.filterFn(value, this.data))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
<h1 mat-dialog-title>Are you sure you want to delete the following ACEs ?</h1>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let ace of data.aces">{{ ace.path }} </li>
|
||||||
|
</ul>
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onCancel()" color="accent">No, cancel</button>
|
||||||
|
<button mat-button (click)="onDelete()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Yes, delete!
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DeleteAceDialogComponent } from './delete-ace-dialog.component';
|
||||||
|
|
||||||
|
describe('DeleteAceDialogComponent', () => {
|
||||||
|
let component: DeleteAceDialogComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteAceDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DeleteAceDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DeleteAceDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2023 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||||
|
import {ACE} from "@models/api/ACE";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-delete-ace-dialog',
|
||||||
|
templateUrl: './delete-ace-dialog.component.html',
|
||||||
|
styleUrls: ['./delete-ace-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class DeleteAceDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private dialogRef: MatDialogRef<DeleteAceDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { aces: ACE[] }) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete() {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ import {ControllerService} from "@services/controller.service";
|
|||||||
export class ManagementComponent implements OnInit {
|
export class ManagementComponent implements OnInit {
|
||||||
|
|
||||||
controller: Controller;
|
controller: Controller;
|
||||||
links = ['users', 'groups', 'roles', 'resourcePools'];
|
links = ['users', 'groups', 'roles', 'resourcePools', 'aces'];
|
||||||
activeLink: string = this.links[0];
|
activeLink: string = this.links[0];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
34
src/app/filters/ace-filter.pipe.ts
Normal file
34
src/app/filters/ace-filter.pipe.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
|
import {User} from "@models/users/user";
|
||||||
|
import {ACE} from "@models/api/ACE";
|
||||||
|
import {Endpoint} from "@models/api/endpoint";
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'aceFilter'
|
||||||
|
})
|
||||||
|
export class AceFilterPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(items: MatTableDataSource<ACE>, searchText: string, endpoints: Endpoint[]){
|
||||||
|
if (!items) return [];
|
||||||
|
if (!searchText) return items;
|
||||||
|
searchText = searchText.toLowerCase()
|
||||||
|
const filteredEndpoints = endpoints.filter((endp: Endpoint) => endp.name.toLowerCase().includes(searchText))
|
||||||
|
return items.data.filter((item: ACE) => {
|
||||||
|
const user = this.getEndpoint(item.user_id, endpoints)
|
||||||
|
const group = this.getEndpoint(item.group_id, endpoints)
|
||||||
|
const path = this.getEndpoint(item.path, endpoints)
|
||||||
|
const role = this.getEndpoint(item.role_id, endpoints)
|
||||||
|
return filteredEndpoints.some((endp: Endpoint) => [user, group, path, role].includes(endp.endpoint))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpoint(id: string, endpoints: Endpoint[]): string {
|
||||||
|
const filter = endpoints.filter((endpoint: Endpoint) => endpoint.endpoint.includes(id))
|
||||||
|
if(filter.length > 0) {
|
||||||
|
return filter[0].endpoint.toLowerCase()
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
18
src/app/models/api/ACE.ts
Normal file
18
src/app/models/api/ACE.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export enum AceType {
|
||||||
|
group= "group",
|
||||||
|
user = "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ACE {
|
||||||
|
ace_id: string;
|
||||||
|
ace_type: AceType;
|
||||||
|
path: string;
|
||||||
|
propagate: boolean;
|
||||||
|
allowed: boolean;
|
||||||
|
user_id?: string;
|
||||||
|
group_id?: string;
|
||||||
|
role_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
17
src/app/models/api/endpoint.ts
Normal file
17
src/app/models/api/endpoint.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export enum RessourceType {
|
||||||
|
project = "project",
|
||||||
|
node = "node",
|
||||||
|
link = "link",
|
||||||
|
user = "user",
|
||||||
|
group = "group",
|
||||||
|
pool = "pool",
|
||||||
|
image = "image",
|
||||||
|
template = "template",
|
||||||
|
root = "root"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Endpoint {
|
||||||
|
endpoint: string,
|
||||||
|
name: string,
|
||||||
|
endpoint_type: RessourceType
|
||||||
|
}
|
16
src/app/services/acl.service.spec.ts
Normal file
16
src/app/services/acl.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AclService } from './acl.service';
|
||||||
|
|
||||||
|
describe('AclService', () => {
|
||||||
|
let service: AclService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AclService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
53
src/app/services/acl.service.ts
Normal file
53
src/app/services/acl.service.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {HttpController} from "@services/http-controller.service";
|
||||||
|
import {ACE} from "@models/api/ACE";
|
||||||
|
import {Endpoint} from "@models/api/endpoint";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AclService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpController: HttpController
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getEndpoints(controller: Controller) {
|
||||||
|
return this.httpController.get<Endpoint[]>(controller, '/access/acl/endpoints')
|
||||||
|
}
|
||||||
|
|
||||||
|
list(controller: Controller) {
|
||||||
|
return this.httpController.get<ACE[]>(controller, '/access/acl');
|
||||||
|
}
|
||||||
|
|
||||||
|
add(controller: Controller, ace: any): Observable<ACE> {
|
||||||
|
return this.httpController.post<ACE>(controller, `/access/acl`, ace);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(controller: Controller, ace_id: string) {
|
||||||
|
return this.httpController.get<ACE>(controller, `/access/acl/${ace_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(controller: Controller, ace_id: string) {
|
||||||
|
return this.httpController.delete(controller, `/access/acl/${ace_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(controller: Controller, ace: ACE): Observable<ACE> {
|
||||||
|
return this.httpController.put<ACE>(controller, `/access/acl/${ace.ace_id}`, ace);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user