} as any;
component.groupForm.get('groupName').setValue('encrypted-group');
- component.groupForm.get('enableEncryption').setValue(true);
+ component.groupForm.get('enable_auth').setValue(true);
component.groupForm.get('encryptionKey').setValue('encryption-key-123');
component.onSubmit();
[CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)]
),
unmanaged: new UntypedFormControl(false),
+ enable_auth: new UntypedFormControl(false),
enableEncryption: new UntypedFormControl(false),
encryptionConfig: new UntypedFormControl(null),
encryptionKey: new UntypedFormControl(null),
unmanaged: formValues.unmanaged
};
- if (formValues.enableEncryption) {
+ if (formValues.enableEncryption || formValues.enable_auth) {
const encryptionKey = formValues.encryptionKey || formValues.encryptionConfig;
if (encryptionKey) {
serviceSpec['encryption_key'] = encryptionKey;
import { SharedModule } from '~/app/shared/shared.module';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
-import { HOST_TYPE } from '~/app/shared/models/nvmeof';
+import { ALLOW_ALL_HOST, HOST_TYPE } from '~/app/shared/models/nvmeof';
import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form.component';
beforeEach(() => {
nvmeofService = TestBed.inject(NvmeofService);
spyOn(nvmeofService, 'addSubsystemInitiators').and.returnValue(of({}));
+ spyOn(nvmeofService, 'removeInitiators').and.returnValue(of({}));
});
it('should be creating request correctly', () => {
expect(nvmeofService.addSubsystemInitiators).not.toHaveBeenCalled();
expect(component.isSubmitLoading).toBe(false);
});
+
+ it('should remove wildcard host before adding specific hosts', () => {
+ const subsystemNQN = 'nqn.test';
+ component.subsystemNQN = subsystemNQN;
+ component.group = 'test-group';
+ component.existingHosts = [ALLOW_ALL_HOST];
+
+ const payload: any = {
+ hostType: HOST_TYPE.SPECIFIC,
+ addedHosts: ['host3']
+ };
+
+ component.onSubmit(payload);
+
+ expect(nvmeofService.removeInitiators).toHaveBeenCalledWith(subsystemNQN, {
+ host_nqn: ALLOW_ALL_HOST,
+ gw_group: 'test-group'
+ });
+ expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith(subsystemNQN, {
+ allow_all: false,
+ gw_group: 'test-group',
+ hosts: [{ dhchap_key: '', host_nqn: 'host3' }]
+ });
+ });
});
});
import { NvmeofService, SubsystemInitiatorRequest } from '~/app/shared/api/nvmeof.service';
import { FinishedTask } from '~/app/shared/models/finished-task';
import {
+ ALLOW_ALL_HOST,
AuthStepType,
HOST_TYPE,
HostStepType,
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component';
+import { Observable } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
type InitiatorsFormPayload = Pick<HostStepType, 'hostType' | 'addedHosts'> &
Partial<Pick<AuthStepType, 'hostDchapKeyList'>>;
hosts,
gw_group: this.group
};
+
+ const shouldRemoveAllowAllHost =
+ payload.hostType === HOST_TYPE.SPECIFIC && this.existingHosts.includes(ALLOW_ALL_HOST);
+
+ const submitCall$: Observable<unknown> = shouldRemoveAllowAllHost
+ ? this.nvmeofService
+ .removeInitiators(this.subsystemNQN, {
+ host_nqn: ALLOW_ALL_HOST,
+ gw_group: this.group
+ })
+ .pipe(
+ switchMap(() => this.nvmeofService.addSubsystemInitiators(this.subsystemNQN, request))
+ )
+ : this.nvmeofService.addSubsystemInitiators(this.subsystemNQN, request);
+
this.taskWrapperService
.wrapTaskAroundCall({
task: new FinishedTask(taskUrl, {
nqn: this.subsystemNQN
}),
- call: this.nvmeofService.addSubsystemInitiators(this.subsystemNQN, request)
+ call: submitCall$
})
.subscribe({
error: () => {
@if (allowAllHosts) {
<cd-alert-panel
- class="cds-mb-4"
- type="info"
- [showTitle]="false">
- <div cdsStack="horizontal"
- gap="2">
- <div cdsStack="vertical"
- gap="2">
- <strong i18n>Host access: All hosts allowed</strong>
- <p
- class="cds-mb-0 cds-mt-1"
- i18n
- >
- Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access.
- </p>
- </div>
- <a cdsLink
- class="cds-ml-3"
- (click)="openAddInitiatorForm(true)"
- i18n>Edit host access</a>
- </div>
- </cd-alert-panel>
-} @else {
- <cd-table [data]="initiators"
- columnMode="flex"
- (fetchData)="listInitiators()"
- [columns]="initiatorColumns"
- selectionType="multiClick"
- (updateSelection)="updateSelection($event)">
- <div class="table-actions">
- <cd-table-actions [permission]="permission"
- [selection]="selection"
- class="btn-group"
- [tableActions]="tableActions">
- </cd-table-actions>
- </div>
- </cd-table>
+ class="cds-mb-4"
+ type="warning"
+ [showTitle]="false">
+ <div cdsStack="vertical"
+ gap="1">
+ <strong class="cds-mb-1"
+ i18n>All hosts allowed</strong>
+ <p class="cds-mb-0"
+ i18n>
+ Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access.
+ </p>
+ </div>
+</cd-alert-panel>
}
+<cd-table [data]="initiators"
+ columnMode="flex"
+ (fetchData)="listInitiators()"
+ [columns]="initiatorColumns"
+ selectionType="multiClick"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions">
+ <cd-table-actions [permission]="permission"
+ [selection]="selection"
+ class="btn-group"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+</cd-table>
+
<ng-template #hostNqnTpl
let-row="data.row">
{{ getDisplayedHostNqn(row.nqn) }}
expect(getSubsystemSpy).toHaveBeenCalledTimes(2);
});
- it('should filter ALLOW_ALL_HOST from array response', fakeAsync(() => {
+ it('should include ALLOW_ALL_HOST from array response', fakeAsync(() => {
spyOn(nvmeofService, 'getInitiators').and.returnValue(
of([
{ nqn: ALLOW_ALL_HOST, use_dhchap: false },
component.listInitiators();
tick();
- expect(component.initiators).toEqual([{ nqn: 'nqn.2016-06.io.spdk:host2', use_dhchap: false }]);
+ expect(component.initiators).toEqual([
+ { nqn: ALLOW_ALL_HOST, use_dhchap: false },
+ { nqn: 'nqn.2016-06.io.spdk:host2', use_dhchap: false }
+ ]);
}));
it('should support hosts-wrapper response from getInitiators', fakeAsync(() => {
component.listInitiators();
tick();
- expect(component.initiators).toEqual([{ nqn: 'nqn.2016-06.io.spdk:host3', use_dhchap: true }]);
+ expect(component.initiators).toEqual([
+ { nqn: ALLOW_ALL_HOST, use_dhchap: false },
+ { nqn: 'nqn.2016-06.io.spdk:host3', use_dhchap: true }
+ ]);
+ }));
+
+ it('should set allowAllHosts to true when wildcard initiator exists', fakeAsync(() => {
+ const allowAllSubsystem = { ...mockSubsystem, allow_any_host: true };
+ spyOn(nvmeofService, 'getInitiators').and.returnValue(
+ of([{ nqn: ALLOW_ALL_HOST, use_dhchap: false }])
+ );
+ spyOn(nvmeofService, 'getSubsystem').and.returnValue(of(allowAllSubsystem));
+
+ component.listInitiators();
+ component.getSubsystem();
+ tick();
+
+ expect(component.allowAllHosts).toBe(true);
}));
});
}
hasAllHostsAllowed(): boolean {
- return !!this.subsystem?.allow_any_host && this.initiators.length === 0;
+ return (
+ !!this.subsystem?.allow_any_host &&
+ (this.initiators.length === 0 ||
+ this.initiators.some((initiator) => initiator.nqn === ALLOW_ALL_HOST))
+ );
}
updateSelection(selection: CdTableSelection) {
.getInitiators(this.subsystemNQN, this.group)
.subscribe((response: NvmeofSubsystemInitiator[] | { hosts: NvmeofSubsystemInitiator[] }) => {
const initiators = Array.isArray(response) ? response : response?.hosts || [];
- this.initiators = initiators.filter((i) => i.nqn !== ALLOW_ALL_HOST);
+ this.initiators = initiators;
this.updateAuthStatus();
});
}
<div class="cds--type-label-02">
<span>{{ detail.value ? 'Allow all hosts' : 'Restrict to specific hosts' }}</span>
<a cdsLink
- [routerLink]="['../hosts']"
- [queryParams]="{ group: groupName }"
+ (click)="openEditHostAccessModal()"
[cdsStack]="'horizontal'"
class="cds-ml-2"
[gap]="1">
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
-import { ActivatedRoute } from '@angular/router';
-import { of } from 'rxjs';
+import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
+import { of, Subject } from 'rxjs';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { GridModule, TilesModule } from 'carbon-components-angular';
import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview.component';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { URLVerbs } from '~/app/shared/constants/app.constants';
import { SharedModule } from '~/app/shared/shared.module';
import { NvmeofSubsystem, NvmeofSubsystemInitiator } from '~/app/shared/models/nvmeof';
let component: NvmeofSubsystemOverviewComponent;
let fixture: ComponentFixture<NvmeofSubsystemOverviewComponent>;
let nvmeofService: NvmeofService;
+ let router: Router;
+ let activatedRoute: ActivatedRoute;
+ let routerEvents$: Subject<any>;
const mockSubsystem: NvmeofSubsystem = {
nqn: 'nqn.2016-06.io.spdk:cnode1',
}
beforeEach(async () => {
+ routerEvents$ = new Subject<any>();
+
await TestBed.configureTestingModule({
declarations: [NvmeofSubsystemOverviewComponent],
imports: [
getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem)),
getInitiators: jest.fn().mockReturnValue(of([]))
}
+ },
+ {
+ provide: Router,
+ useValue: {
+ events: routerEvents$.asObservable(),
+ navigate: jest.fn().mockResolvedValue(true)
+ }
}
]
}).compileComponents();
fixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent);
component = fixture.componentInstance;
nvmeofService = TestBed.inject(NvmeofService);
+ router = TestBed.inject(Router);
+ activatedRoute = TestBed.inject(ActivatedRoute);
fixture.detectChanges();
});
});
expect(f.nativeElement.textContent).toContain('Unidirectional');
}));
+
+ it('should open host access edit modal route when edit is clicked', () => {
+ const navigateSpy = jest.spyOn(router, 'navigate').mockResolvedValue(true);
+ component.groupName = 'group1';
+ component.openEditHostAccessModal();
+
+ expect(navigateSpy).toHaveBeenCalledWith(
+ [{ outlets: { modal: [URLVerbs.ADD, 'initiator'] } }],
+ {
+ queryParams: { group: 'group1' },
+ relativeTo: activatedRoute.parent
+ }
+ );
+ });
+
+ it('should refresh subsystem on non-modal navigation end', () => {
+ (nvmeofService.getSubsystem as jest.Mock).mockClear();
+
+ routerEvents$.next(new NavigationEnd(1, '/nvmeof/(modal:add)', '/nvmeof/(modal:add)'));
+ expect(nvmeofService.getSubsystem).not.toHaveBeenCalled();
+
+ routerEvents$.next(
+ new NavigationEnd(2, '/nvmeof/subsystems/overview', '/nvmeof/subsystems/overview')
+ );
+ expect(nvmeofService.getSubsystem).toHaveBeenCalledWith('nqn.2016-06.io.spdk:cnode1', 'group1');
+ });
});
-import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { forkJoin } from 'rxjs';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
+import { forkJoin, Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
import {
NO_AUTH,
getSubsystemAuthStatus
} from '~/app/shared/models/nvmeof';
+import { URLVerbs } from '~/app/shared/constants/app.constants';
import { ICON_TYPE } from '~/app/shared/enum/icons.enum';
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
import { NvmeofEditAuthenticationComponent } from '../nvmeof-edit-authentication/nvmeof-edit-authentication.component';
styleUrls: ['./nvmeof-subsystem-overview.component.scss'],
standalone: false
})
-export class NvmeofSubsystemOverviewComponent implements OnInit {
+export class NvmeofSubsystemOverviewComponent implements OnInit, OnDestroy {
subsystemNQN!: string;
groupName!: string;
subsystem!: NvmeofSubsystem;
details: SubsystemDetail[] = [];
+ private subscriptions = new Subscription();
constructor(
private route: ActivatedRoute,
+ private router: Router,
private nvmeofService: NvmeofService,
private modalService: ModalCdsService
) {}
ngOnInit() {
- this.route.parent?.params.subscribe((params) => {
- this.subsystemNQN = params['subsystem_nqn'];
- this.fetchIfReady();
- });
- this.route.queryParams.subscribe((qp) => {
- this.groupName = qp['group'];
- this.fetchIfReady();
- });
+ this.subscriptions.add(
+ this.route.parent?.params.subscribe((params) => {
+ this.subsystemNQN = params['subsystem_nqn'];
+ this.fetchIfReady();
+ })
+ );
+ this.subscriptions.add(
+ this.route.queryParams.subscribe((qp) => {
+ this.groupName = qp['group'];
+ this.fetchIfReady();
+ })
+ );
+ this.subscriptions.add(
+ this.router.events
+ .pipe(
+ filter(
+ (event): event is NavigationEnd =>
+ event instanceof NavigationEnd && !event.urlAfterRedirects.includes('(modal:')
+ )
+ )
+ .subscribe(() => {
+ this.fetchIfReady();
+ })
+ );
+ }
+
+ ngOnDestroy() {
+ this.subscriptions.unsubscribe();
}
private fetchIfReady() {
modalRef.closeChange.subscribe(() => this.fetchSubsystem());
}
}
+
+ openEditHostAccessModal() {
+ this.router.navigate([{ outlets: { modal: [URLVerbs.ADD, 'initiator'] } }], {
+ queryParams: { group: this.groupName },
+ relativeTo: this.route.parent
+ });
+ }
}