Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/app/interfaces/api/api-job-directory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface ApiJobDirectory {
// Directory Services
'directoryservices.cache_refresh': { params: void; response: void };
'directoryservices.leave': { params: [DirectoryServicesLeaveParams]; response: void };
'directoryservices.sync_keytab': { params: void; response: void };
'directoryservices.update': { params: [DirectoryServicesUpdate]; response: DirectoryServicesUpdateResponse };

// Disk
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
@if (!inCard()) {
<ix-page-header>
<ix-search-input1 [value]="filterString" (search)="onListFiltered($event)"></ix-search-input1>
@if (isActiveDirectoryEnabled()) {
<button
*ixRequiresRoles="requiredRoles"
mat-button
ixTest="sync-kerberos-keytab"
[ixUiSearch]="searchableElements.elements.sync"
(click)="syncKeytab()"
>
{{ 'Sync' | translate }}
</button>
}
<button
*ixRequiresRoles="requiredRoles"
mat-button
Expand Down Expand Up @@ -29,6 +40,16 @@ <h3>{{ 'Kerberos Keytab' | translate }}</h3>
></ix-icon>
</div>
<div class="action">
@if (isActiveDirectoryEnabled()) {
<button
*ixRequiresRoles="requiredRoles"
mat-button
ixTest="sync-kerberos-keytab"
(click)="syncKeytab()"
>
{{ 'Sync' | translate }}
</button>
}
<button
*ixRequiresRoles="requiredRoles"
mat-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@
display: flex;
justify-content: space-between;
}

.action {
display: flex;
gap: 8px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { mockApi, mockCall } from 'app/core/testing/utils/mock-api.utils';
import { mockApi, mockCall, mockJob } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { DirectoryServiceStatus, DirectoryServiceType } from 'app/enums/directory-services.enum';
import { DirectoryServicesStatus } from 'app/interfaces/directoryservices-status.interface';
import { KerberosKeytab } from 'app/interfaces/kerberos-config.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxTableHarness } from 'app/modules/ix-table/components/ix-table/ix-table.harness';
import { SlideIn } from 'app/modules/slide-ins/slide-in';
import { ApiService } from 'app/modules/websocket/api.service';
Expand Down Expand Up @@ -41,7 +44,18 @@ describe('KerberosKeytabsListComponent', () => {
name: 'keytab2',
},
] as KerberosKeytab[]),
mockCall('directoryservices.status', {
type: DirectoryServiceType.ActiveDirectory,
status: DirectoryServiceStatus.Healthy,
} as DirectoryServicesStatus),
mockJob('directoryservices.sync_keytab'),
]),
mockProvider(DialogService, {
confirm: jest.fn(() => of(true)),
jobDialog: jest.fn(() => ({
afterClosed: () => of(null),
})),
}),
mockAuth(),
],
});
Expand Down Expand Up @@ -69,4 +83,49 @@ describe('KerberosKeytabsListComponent', () => {

expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(KerberosKeytabsFormComponent);
});

it('calls directoryservices.sync_keytab when Sync is pressed', async () => {
const syncButton = await loader.getHarness(MatButtonHarness.with({ text: 'Sync' }));
await syncButton.click();

expect(spectator.inject(DialogService).confirm).toHaveBeenCalled();
expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled();
expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('directoryservices.sync_keytab');
});
});

describe('KerberosKeytabsListComponent - LDAP mode', () => {
let spectator: Spectator<KerberosKeytabsListComponent>;
let loader: HarnessLoader;

const createComponent = createComponentFactory({
component: KerberosKeytabsListComponent,
declarations: [
MockComponent(KerberosKeytabsFormComponent),
],
providers: [
mockProvider(SlideIn, {
open: jest.fn(() => of()),
}),
mockApi([
mockCall('kerberos.keytab.query', []),
mockCall('directoryservices.status', {
type: DirectoryServiceType.Ldap,
status: DirectoryServiceStatus.Healthy,
} as DirectoryServicesStatus),
]),
mockProvider(DialogService),
mockAuth(),
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('does not show Sync button when AD is disabled', async () => {
const syncButtons = await loader.getAllHarnesses(MatButtonHarness.with({ text: 'Sync' }));
expect(syncButtons).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, input, OnInit, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, input, OnInit, inject, signal } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatToolbarRow } from '@angular/material/toolbar';
import { MatTooltip } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { filter, switchMap, tap } from 'rxjs';
import { filter, switchMap } from 'rxjs';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
import { UiSearchDirective } from 'app/directives/ui-search.directive';
import { DirectoryServiceStatus, DirectoryServiceType } from 'app/enums/directory-services.enum';
import { Role } from 'app/enums/role.enum';
import { KerberosKeytab } from 'app/interfaces/kerberos-config.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
Expand All @@ -28,6 +29,7 @@ import { SortDirection } from 'app/modules/ix-table/enums/sort-direction.enum';
import { createTable } from 'app/modules/ix-table/utils';
import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component';
import { SlideIn } from 'app/modules/slide-ins/slide-in';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApiService } from 'app/modules/websocket/api.service';
import { KerberosKeytabsFormComponent } from 'app/pages/directory-service/components/kerberos-keytabs/kerberos-keytabs-form/kerberos-keytabs-form.component';
Expand Down Expand Up @@ -67,16 +69,18 @@ export class KerberosKeytabsListComponent implements OnInit {
private errorHandler = inject(ErrorHandlerService);
protected emptyService = inject(EmptyService);
private slideIn = inject(SlideIn);
private snackbar = inject(SnackbarService);

readonly paginator = input(true);
readonly inCard = input(false);
readonly showSyncButton = input(false);

protected readonly requiredRoles = [Role.DirectoryServiceWrite];
protected readonly searchableElements = kerberosKeytabsListElements;

protected readonly isActiveDirectoryEnabled = signal(false);
filterString = '';
dataProvider: AsyncDataProvider<KerberosKeytab>;
kerberosRealsm: KerberosKeytab[] = [];
columns = createTable<KerberosKeytab>([
textColumn({
title: this.translate.instant('Name'),
Expand Down Expand Up @@ -124,16 +128,30 @@ export class KerberosKeytabsListComponent implements OnInit {
});

ngOnInit(): void {
const keytabsRows$ = this.api.call('kerberos.keytab.query').pipe(
tap((keytabsRows) => this.kerberosRealsm = keytabsRows),
untilDestroyed(this),
);
const keytabsRows$ = this.api.call('kerberos.keytab.query');
this.dataProvider = new AsyncDataProvider<KerberosKeytab>(keytabsRows$);
this.setDefaultSort();
this.getKerberosKeytabs();
this.dataProvider.emptyType$.pipe(untilDestroyed(this)).subscribe(() => {
this.onListFiltered(this.filterString);
});

if (this.inCard()) {
this.isActiveDirectoryEnabled.set(this.showSyncButton());
} else {
this.checkActiveDirectoryStatus();
}
}

private checkActiveDirectoryStatus(): void {
this.api.call('directoryservices.status').pipe(
untilDestroyed(this),
).subscribe((status) => {
this.isActiveDirectoryEnabled.set(
status.type === DirectoryServiceType.ActiveDirectory
&& status.status !== DirectoryServiceStatus.Disabled,
);
});
}

getKerberosKeytabs(): void {
Expand All @@ -159,4 +177,35 @@ export class KerberosKeytabsListComponent implements OnInit {
this.filterString = query;
this.dataProvider.setFilter({ query, columnKeys: ['name'] });
}

syncKeytab(): void {
if (!this.isActiveDirectoryEnabled()) {
return;
}

this.dialogService.confirm({
title: this.translate.instant('Sync Keytab'),
message: this.translate.instant('This operation synchronizes the local kerberos keytab with entries from the remote domain controller. This may be disruptive and is only required if manual changes have been made to SPNs on the remote domain controller. Do you want to continue?'),
buttonText: this.translate.instant('Sync'),
})
.pipe(
filter(Boolean),
switchMap(() => {
return this.dialogService.jobDialog(
this.api.job('directoryservices.sync_keytab'),
{ title: this.translate.instant('Sync Keytab') },
).afterClosed();
}),
untilDestroyed(this),
)
.subscribe({
next: () => {
this.snackbar.success(this.translate.instant('Keytab synchronized successfully.'));
this.getKerberosKeytabs();
},
error: (error: unknown) => {
this.errorHandler.showErrorModal(error);
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,13 @@ export const kerberosKeytabsListElements = {
T('Kerberos Keytab'),
],
},
sync: {
hierarchy: [T('Sync Kerberos Keytab')],
anchor: 'sync-kerberos-keytab',
synonyms: [
T('Synchronize Kerberos Keytab'),
T('Sync Keytab'),
],
},
},
} satisfies UiSearchableElement;
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ <h3 class="header">
@if (expansionPanel.expanded) {
<div class="advanced-settings-cards">
<ix-kerberos-realms-list [paginator]="false" [inCard]="true"></ix-kerberos-realms-list>
<ix-kerberos-keytabs-list [paginator]="false" [inCard]="true"></ix-kerberos-keytabs-list>
<ix-kerberos-keytabs-list [paginator]="false" [inCard]="true" [showSyncButton]="isActiveDirectoryEnabled"></ix-kerberos-keytabs-list>
</div>
}
</cdk-accordion-item>
Expand Down
Loading