Corrected ou-operator bug

pull/97/head
Manuel Aranda Rosales 2026-07-29 00:02:36 +02:00
parent 348df41425
commit 2b77eb8afa
11 changed files with 73 additions and 38 deletions

View File

@ -54,6 +54,11 @@ test.describe('OU Operator — solo grupos y operaciones de equipos', () => {
const finalUrl = await navigateTo(page, '/clients/run-script');
expect(finalUrl).toContain('/clients/run-script');
});
test('accede a /commands-logs (trazas de sus acciones)', async ({ page }) => {
const finalUrl = await navigateTo(page, '/commands-logs');
expect(finalUrl).toContain('/commands-logs');
});
});
// ── GUI en /groups: botones de crear OU/cliente ocultos ─────────────────────

View File

@ -69,7 +69,7 @@ const routes: Routes = [
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },

View File

@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { AuthService } from '@services/auth.service';
@Component({
selector: 'app-global-status',
@ -67,7 +68,8 @@ export class GlobalStatusComponent implements OnInit {
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient
private http: HttpClient,
private auth: AuthService
) {
this.baseUrl = this.configService.apiUrl;
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
@ -115,7 +117,18 @@ export class GlobalStatusComponent implements OnInit {
});
}
/**
* Las sincronizaciones con la infraestructura (subredes DHCP, plantillas PXE y
* ogLives) están restringidas a ROLE_SUPER_ADMIN en ogCore; para el resto de
* perfiles devuelven "Access Denied", así que ni siquiera se lanzan.
*/
private get canSyncInfrastructure(): boolean {
return this.auth.isSuperAdmin;
}
syncSubnets() {
if (!this.canSyncInfrastructure) return;
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(data => {
const dhcps = data['hydra:member'] || [];
if (dhcps.length === 0) return;
@ -155,6 +168,7 @@ export class GlobalStatusComponent implements OnInit {
}
syncTemplates() {
if (!this.canSyncInfrastructure) return;
if (this.bootServers.length === 0) return;
let pending = this.bootServers.length;
@ -189,6 +203,7 @@ export class GlobalStatusComponent implements OnInit {
}
syncOgLives(): void {
if (!this.canSyncInfrastructure) return;
if (this.bootServers.length === 0) return;
let pending = this.bootServers.length;

View File

@ -64,10 +64,14 @@ export class LoginComponent {
this.auth.refresh();
this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
this.router.navigateByUrl('/groups');
this.dialog.open(GlobalStatusComponent, {
width: '65vw',
height: '80vh',
});
// El estado global consulta y sincroniza infraestructura (ogBoot, DHCP,
// repositorios), a la que solo tienen acceso los perfiles de administración.
if (this.auth.isSuperAdmin || this.auth.isOUAdmin) {
this.dialog.open(GlobalStatusComponent, {
width: '65vw',
height: '80vh',
});
}
}
this.isLoading = false;
},

View File

@ -105,19 +105,19 @@ export class ClientPendingTasksComponent implements OnInit {
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data && data['@id']) {
this.updateTracesStatus(data['@id'], data.status);
this.updateTraceFromEvent(data['@id'], data);
}
}
}
private updateTracesStatus(clientUuid: string, newStatus: string): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
private updateTraceFromEvent(traceId: string, data: any): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === traceId);
if (traceIndex !== -1) {
const updatedTraces = [...this.traces];
updatedTraces[traceIndex] = {
...updatedTraces[traceIndex],
status: newStatus
...data
};
this.traces = updatedTraces;

View File

@ -107,28 +107,23 @@ export class ClientTaskLogsComponent implements OnInit {
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data && data['@id']) {
this.updateTracesStatus(data['@id'], data.status, data.progress);
this.updateTraceFromEvent(data['@id'], data);
}
}
}
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
private updateTraceFromEvent(traceId: string, data: any): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === traceId);
if (traceIndex !== -1) {
const updatedTraces = [...this.traces];
updatedTraces[traceIndex] = {
...updatedTraces[traceIndex],
status: newStatus,
progress: progress
...data
};
this.traces = updatedTraces;
this.cdr.detectChanges();
console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`);
} else {
console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`);
}
}

View File

@ -157,7 +157,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data && data['@id']) {
this.updateTracesStatus(data['@id'], data.status, data.progress);
this.updateTraceFromEvent(data['@id'], data);
}
}
}
@ -439,15 +439,14 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
});
}
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
private updateTraceFromEvent(traceId: string, data: any): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === traceId);
if (traceIndex !== -1) {
const updatedTraces = [...this.traces];
updatedTraces[traceIndex] = {
...updatedTraces[traceIndex],
status: newStatus,
progress: progress
...data
};
this.traces = updatedTraces;

View File

@ -139,7 +139,6 @@ describe('roleGuard — control de acceso por rol', () => {
// super-admin + ou-admin
{ path: 'commands', allowedRoles: ['super-admin', 'ou-admin'] },
{ path: 'commands-task', allowedRoles: ['super-admin', 'ou-admin'] },
{ path: 'commands-logs', allowedRoles: ['super-admin', 'ou-admin'] },
{ path: 'pxe-images', allowedRoles: ['super-admin', 'ou-admin'] },
{ path: 'pxe', allowedRoles: ['super-admin', 'ou-admin'] },
{ path: 'pxe-boot-file', allowedRoles: ['super-admin', 'ou-admin'] },
@ -156,6 +155,7 @@ describe('roleGuard — control de acceso por rol', () => {
{ path: 'hardware-profiles', allowedRoles: ['super-admin', 'ou-admin'] },
{ path: 'hardware-types', allowedRoles: ['super-admin', 'ou-admin'] },
// super-admin + ou-admin + ou-operator
{ path: 'commands-logs', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
{ path: 'clients/deploy-image', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
{ path: 'clients/partition-assistant',allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
{ path: 'clients/run-script', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },

View File

@ -21,9 +21,11 @@
{{ 'downloadAgent' | translate }}
</a>
<button class="ordinary-button" (click)="showGlobalStatus()">
{{ 'GlobalStatus' | translate }}
</button>
@if (auth.userCategory === 'super-admin' || auth.userCategory === 'ou-admin') {
<button class="ordinary-button" (click)="showGlobalStatus()">
{{ 'GlobalStatus' | translate }}
</button>
}
@if (auth.userCategory === 'super-admin') {
<button class="ordinary-button" [matMenuTriggerFor]="menu"
@ -75,9 +77,11 @@
<mat-icon>download</mat-icon>
{{ 'downloadAgent' | translate }}
</a>
<button mat-menu-item (click)="showGlobalStatus()">
{{ 'GlobalStatus' | translate }}
</button>
@if (auth.userCategory === 'super-admin' || auth.userCategory === 'ou-admin') {
<button mat-menu-item (click)="showGlobalStatus()">
{{ 'GlobalStatus' | translate }}
</button>
}
@if (auth.userCategory === 'super-admin') {
<button mat-menu-item [matMenuTriggerFor]="menu">
{{ 'Administration' | translate }}

View File

@ -18,7 +18,7 @@
</mat-list-item>
<mat-list-item (click)="toggleCommandSub()" matTooltip="{{ 'TOOLTIP_ACTIONS' | translate }}"
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-minimal'">
<span class="entry">
<mat-icon class="icon">playlist_play</mat-icon>
<span>{{ 'actions' | translate }}</span>
@ -51,7 +51,7 @@
</span>
</mat-list-item>
<mat-list-item routerLink="/commands-logs" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_TRACE_LOGS' | translate }}"
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-minimal'">
<span class="entry">
<mat-icon class="icon">chevron_right</mat-icon>
<span>{{ 'traceLogs' | translate }}</span>

View File

@ -116,8 +116,8 @@ describe('SidebarComponent — visibilidad de elementos por rol', () => {
});
});
// ── Ou-operator: solo /groups ──────────────────────────────────────────────
describe('ou-operator — solo ve /groups', () => {
// ── Ou-operator: /groups y las trazas de sus acciones ──────────────────────
describe('ou-operator — ve /groups y /commands-logs', () => {
beforeEach(async () => createComponent('ou-operator'));
it('ve /groups', () => {
@ -130,9 +130,22 @@ describe('SidebarComponent — visibilidad de elementos por rol', () => {
});
});
it('NO ve el menú de acciones (commands)', () => {
// Solo debe haber 1 mat-list-item visible: /groups
expect(countVisibleListItems(fixture)).toBe(1);
it('ve el menú de acciones (commands) para acceder a las trazas', () => {
// /groups + el desplegable de acciones
expect(countVisibleListItems(fixture)).toBe(2);
});
it('ve el enlace a /commands-logs al desplegar el menú de acciones', () => {
component.toggleCommandSub();
fixture.detectChanges();
expect(hasRouteLink(fixture, '/commands-logs')).toBeTrue();
});
it('NO ve los enlaces de gestión de comandos ni tareas', () => {
component.toggleCommandSub();
fixture.detectChanges();
expect(hasRouteLink(fixture, '/commands')).toBeFalse();
expect(hasRouteLink(fixture, '/commands-task')).toBeFalse();
});
});