Skip to content

Commit

Permalink
NAS-133442 / 25.04 / hide SED related options in UI (#11328)
Browse files Browse the repository at this point in the history
* NAS-133442: hide SED related options in UI

* NAS-133442: hide SED related options in UI

* NAS-133442: hide SED related options in UI

* NAS-133442: hide SED related options in UI

* NAS-133442: hide SED related options in UI

* NAS-133442: hide SED related options in UI
  • Loading branch information
denysbutenko authored Jan 21, 2025
1 parent 478af04 commit fdda7b3
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 173 deletions.
13 changes: 7 additions & 6 deletions src/app/pages/credentials/kmip/kmip.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,13 @@
</div>
</ix-fieldset>
<ix-fieldset [title]="'Management' | translate">
<ix-checkbox
formControlName="manage_sed_disks"
[label]="'Manage SED Passwords' | translate"
[tooltip]="helptext.manage_sed_disks.tooltip | translate"
></ix-checkbox>

@if (allowSedManage()) {
<ix-checkbox
formControlName="manage_sed_disks"
[label]="'Manage SED Passwords' | translate"
[tooltip]="helptext.manage_sed_disks.tooltip | translate"
></ix-checkbox>
}
<ix-checkbox
formControlName="manage_zfs_keys"
[label]="'Manage ZFS Keys' | translate"
Expand Down
23 changes: 22 additions & 1 deletion src/app/pages/credentials/kmip/kmip.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { HarnessLoader, parallel } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { of } from 'rxjs';
import { MockApiService } from 'app/core/testing/classes/mock-api.service';
import {
Expand All @@ -11,13 +12,15 @@ import {
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { helptextSystemKmip } from 'app/helptext/system/kmip';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxCheckboxHarness } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.harness';
import {
WithManageCertificatesLinkComponent,
} from 'app/modules/forms/ix-forms/components/with-manage-certificates-link/with-manage-certificates-link.component';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { ApiService } from 'app/modules/websocket/api.service';
import { SystemGeneralService } from 'app/services/system-general.service';
import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors';
import { KmipComponent } from './kmip.component';

describe('KmipComponent', () => {
Expand Down Expand Up @@ -45,6 +48,7 @@ describe('KmipComponent', () => {
mockCall('kmip.kmip_sync_pending', false),
mockCall('kmip.clear_sync_pending_keys'),
mockCall('kmip.sync_keys'),
mockCall('system.advanced.sed_global_password_is_set', false),
mockJob('kmip.update'),
]),
mockProvider(DialogService, {
Expand All @@ -64,6 +68,12 @@ describe('KmipComponent', () => {
]),
}),
mockAuth(),
provideMockStore({
selectors: [{
selector: selectIsEnterprise,
value: true,
}],
}),
],
});

Expand Down Expand Up @@ -170,4 +180,15 @@ describe('KmipComponent', () => {
);
});
});

it('checks no "Manage SED Passwords" checkbox are present when community edition', async () => {
const store$ = spectator.inject(MockStore);
store$.overrideSelector(selectIsEnterprise, false);
store$.refreshState();

const checkboxes = await loader.getAllHarnesses(IxCheckboxHarness);
const checkLabels = await parallel(() => checkboxes.map((control) => control.getLabelText()));

expect(checkLabels.find((label) => label === 'Manage SED Passwords')).not.toExist();
});
});
11 changes: 10 additions & 1 deletion src/app/pages/credentials/kmip/kmip.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit,
ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, OnInit,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatProgressBar } from '@angular/material/progress-bar';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Store } from '@ngrx/store';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { forkJoin } from 'rxjs';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
Expand All @@ -27,6 +29,8 @@ import { ApiService } from 'app/modules/websocket/api.service';
import { kmipElements } from 'app/pages/credentials/kmip/kmip.elements';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { SystemGeneralService } from 'app/services/system-general.service';
import { AppState } from 'app/store';
import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors';

@UntilDestroy()
@Component({
Expand Down Expand Up @@ -78,6 +82,10 @@ export class KmipComponent implements OnInit {
readonly certificates$ = this.systemGeneralService.getCertificates().pipe(idNameArrayToOptions());
readonly certificateAuthorities$ = this.systemGeneralService.getCertificateAuthorities().pipe(idNameArrayToOptions());

protected readonly hasGlobalEncryption = toSignal(this.api.call('system.advanced.sed_global_password_is_set'));
protected readonly isEnterprise = toSignal(this.store$.select(selectIsEnterprise));
protected readonly allowSedManage = computed(() => this.isEnterprise() || this.hasGlobalEncryption());

constructor(
private api: ApiService,
private formBuilder: FormBuilder,
Expand All @@ -87,6 +95,7 @@ export class KmipComponent implements OnInit {
private dialogService: DialogService,
private systemGeneralService: SystemGeneralService,
private snackbar: SnackbarService,
private store$: Store<AppState>,
) {}

ngOnInit(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@if (hasSedSupport()) {
<mat-card class="card">
<mat-card-header>
<h3 mat-card-title>{{ 'Hardware Disk Encryption' | translate }}</h3>
Expand All @@ -6,13 +7,15 @@ <h3 mat-card-title>{{ 'Hardware Disk Encryption' | translate }}</h3>
<mat-card-content>
<div class="details-item">
<div class="label">{{ 'SED Password' | translate }}:</div>
<ng-container *ixWithLoadingState="hasDiskEncryption$ as hasDiskEncryption">
<div class="value">
{{ hasDiskEncryption ? ('Password is set' | translate) : ('Password is not set' | translate) }}
</div>
</ng-container>
<div class="value">
{{ hasDiskEncryption()
? ('Password is set' | translate)
: ('Password is not set' | translate)
}}
</div>

<a
*ixHasRole="[Role.FullAdmin]"
*ixHasRole="requiredRoles"
ixTest="manage-sed-password"
(click)="onManageSedPassword()"
>
Expand All @@ -22,18 +25,21 @@ <h3 mat-card-title>{{ 'Hardware Disk Encryption' | translate }}</h3>

<div class="details-item">
<div class="label">{{ 'Global SED Password' | translate }}:</div>
<ng-container *ixWithLoadingState="hasGlobalEncryption$ as hasGlobalEncryption">
<div class="value">
{{ hasGlobalEncryption ? ('Password is set' | translate) : ('Password is not set' | translate) }}
{{ hasGlobalEncryption()
? ('Password is set' | translate)
: ('Password is not set' | translate)
}}
</div>
</ng-container>
<a
*ixHasRole="[Role.FullAdmin]"
ixTest="manage-global-sed-password"
[routerLink]="['/system/advanced']"
>
{{ 'Manage Global SED Password' | translate }}
</a>

<a
*ixHasRole="requiredRoles"
ixTest="manage-global-sed-password"
[routerLink]="['/system/advanced']"
>
{{ 'Manage Global SED Password' | translate }}
</a>
</div>
</mat-card-content>
</mat-card>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MatDialog } from '@angular/material/dialog';
import {
byText, createComponentFactory, Spectator, mockProvider,
} from '@ngneat/spectator/jest';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { of } from 'rxjs';
import { mockCall, mockApi } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
Expand All @@ -12,10 +13,13 @@ import { ApiService } from 'app/modules/websocket/api.service';
import {
ManageDiskSedDialogComponent,
} from 'app/pages/storage/modules/devices/components/hardware-disk-encryption/manage-disk-sed-dialog/manage-disk-sed-dialog.component';
import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors';
import { HardwareDiskEncryptionComponent } from './hardware-disk-encryption.component';

describe('HardwareDiskEncryptionComponent', () => {
let spectator: Spectator<HardwareDiskEncryptionComponent>;
let store$: MockStore;

const createComponent = createComponentFactory({
component: HardwareDiskEncryptionComponent,
imports: [
Expand All @@ -24,14 +28,20 @@ describe('HardwareDiskEncryptionComponent', () => {
providers: [
mockApi([
mockCall('disk.query', [{ passwd: '' } as Disk]),
mockCall('system.advanced.sed_global_password_is_set', true),
mockCall('system.advanced.sed_global_password_is_set', false),
]),
mockProvider(MatDialog, {
open: jest.fn(() => ({
afterClosed: () => of(),
afterClosed: () => of(false),
})),
}),
mockAuth(),
provideMockStore({
selectors: [{
selector: selectIsEnterprise,
value: false,
}],
}),
],
});

Expand All @@ -43,31 +53,52 @@ describe('HardwareDiskEncryptionComponent', () => {
} as TopologyDisk,
},
});
store$ = spectator.inject(MockStore);
});

it('loads and shows whether password is set for the current disk', () => {
expect(spectator.inject(ApiService).call)
.toHaveBeenCalledWith('disk.query', [[['devname', '=', 'sda']], { extra: { passwords: true } }]);
describe('no SED support', () => {
beforeEach(() => {
store$.overrideSelector(selectIsEnterprise, true);
store$.refreshState();
spectator.detectChanges();
});

const detailsItem = spectator.query(byText('SED Password:', { exact: true }))!;
expect(detailsItem.nextElementSibling).toHaveText('Password is not set');
it('checks no hardware disk encryption support', () => {
expect(spectator.query('.mat-card')).not.toExist();
});
});

it('loads and shows whether SED password is set globally', () => {
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('system.advanced.sed_global_password_is_set');
describe('with SED support', () => {
beforeEach(() => {
store$.overrideSelector(selectIsEnterprise, true);
store$.refreshState();
spectator.detectChanges();
});

const detailsItem = spectator.query(byText('Global SED Password:', { exact: true }))!;
expect(detailsItem.nextElementSibling).toHaveText('Password is set');
});
it('loads and shows whether password is set for the current disk', () => {
expect(spectator.inject(ApiService).call)
.toHaveBeenCalledWith('disk.query', [[['devname', '=', 'sda']], { extra: { passwords: true } }]);

it('opens a ManageDiskSedDialogComponent when user clicks on Manage SED Password', () => {
spectator.click(spectator.query(byText('Manage SED Password'))!);
const detailsItem = spectator.query(byText('SED Password:', { exact: true }))!;
expect(detailsItem.nextElementSibling).toHaveText('Password is not set');
});

expect(spectator.inject(MatDialog).open).toHaveBeenCalledWith(ManageDiskSedDialogComponent, { data: 'sda' });
});
it('loads and shows whether SED password is set globally', () => {
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('system.advanced.sed_global_password_is_set');

const detailsItem = spectator.query(byText('Global SED Password:', { exact: true }))!;
expect(detailsItem.nextElementSibling).toHaveText('Password is not set');
});

it('shows a link to manage global SED password', () => {
const link = spectator.query(byText('Manage Global SED Password'));
expect(link).toHaveAttribute('href', '/system/advanced');
it('shows a link to manage SED password and opens dialog', () => {
const manageSedPassword = spectator.query(byText('Manage SED Password'));
spectator.click(manageSedPassword);
expect(spectator.inject(MatDialog).open).toHaveBeenCalledWith(ManageDiskSedDialogComponent, { data: 'sda' });
});

it('shows a link to manage global SED password', () => {
const manageGlobalSedPassword = spectator.query(byText('Manage Global SED Password'));
expect(manageGlobalSedPassword).toHaveAttribute('href', '/system/advanced');
});
});
});
Loading

0 comments on commit fdda7b3

Please sign in to comment.