Skip to content

Commit

Permalink
NAS-133464 / 25.04 / Disable token authentication when STIG is enabled (
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko authored Jan 22, 2025
1 parent 555ecce commit 12a16c1
Show file tree
Hide file tree
Showing 96 changed files with 179 additions and 30 deletions.
6 changes: 6 additions & 0 deletions src/app/interfaces/auth.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ export enum LoginExMechanism {
ApiKeyPlain = 'API_KEY_PLAIN',
}

export enum AuthenticatorLoginLevel {
Level1 = 'LEVEL_1',
Level2 = 'LEVEL_2',
}

export interface LoginSuccessResponse {
response_type: LoginExResponseType.Success;
user_info: LoggedInUser;
authenticator: AuthenticatorLoginLevel;
}

export interface LoginAuthErrorResponse {
Expand Down
34 changes: 33 additions & 1 deletion src/app/modules/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { LoginResult } from 'app/enums/login-result.enum';
import { Role } from 'app/enums/role.enum';
import { WINDOW } from 'app/helpers/window.helper';
import { LoginExMechanism, LoginExResponse, LoginExResponseType } from 'app/interfaces/auth.interface';
import {
AuthenticatorLoginLevel, LoginExMechanism, LoginExResponse, LoginExResponseType,
} from 'app/interfaces/auth.interface';
import { DashConfigItem } from 'app/interfaces/dash-config-item.interface';
import { LoggedInUser } from 'app/interfaces/ds-cache.interface';
import { Preferences } from 'app/interfaces/preferences.interface';
Expand Down Expand Up @@ -58,6 +60,7 @@ describe('AuthService', () => {
mockCall('auth.generate_token', 'DUMMY_TOKEN'),
mockCall('auth.logout'),
mockCall('auth.login_ex', {
authenticator: AuthenticatorLoginLevel.Level1,
response_type: LoginExResponseType.Success,
user_info: {
privilege: { webui_access: true },
Expand Down Expand Up @@ -135,6 +138,35 @@ describe('AuthService', () => {
expect(spectator.inject(ApiService).call).not.toHaveBeenCalledWith('auth.me');
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('auth.generate_token');
});

it('initializes auth session with LEVEL_2 with no token support.', () => {
timer$.next(0);

// Mock the auth.login_ex response for LEVEL_2 authentication
spectator.inject(MockApiService).mockCall('auth.login_ex', {
authenticator: AuthenticatorLoginLevel.Level2,
response_type: LoginExResponseType.Success,
user_info: {
privilege: { webui_access: true },
},
} as LoginExResponse);

const obs$ = spectator.service.login('dummy', 'dummy');

testScheduler.run(({ expectObservable }) => {
expectObservable(obs$).toBe(
'(a|)',
{ a: LoginResult.Success },
);
});

expect(spectator.inject(ApiService).call).toHaveBeenCalledWith(
'auth.login_ex',
[{ mechanism: 'PASSWORD_PLAIN', username: 'dummy', password: 'dummy' }],
);
expect(spectator.inject(ApiService).call).not.toHaveBeenCalledWith('auth.me');
expect(spectator.inject(ApiService).call).not.toHaveBeenCalledWith('auth.generate_token');
});
});

describe('Logout', () => {
Expand Down
43 changes: 28 additions & 15 deletions src/app/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import {
import { AccountAttribute } from 'app/enums/account-attribute.enum';
import { LoginResult } from 'app/enums/login-result.enum';
import { Role } from 'app/enums/role.enum';
import { filterAsync } from 'app/helpers/operators/filter-async.operator';
import { WINDOW } from 'app/helpers/window.helper';
import { LoginExMechanism, LoginExResponse, LoginExResponseType } from 'app/interfaces/auth.interface';
import {
AuthenticatorLoginLevel, LoginExMechanism, LoginExResponse, LoginExResponseType,
} from 'app/interfaces/auth.interface';
import { LoggedInUser } from 'app/interfaces/ds-cache.interface';
import { GlobalTwoFactorConfig } from 'app/interfaces/two-factor-config.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
Expand Down Expand Up @@ -56,6 +59,7 @@ export class AuthService {
private generateTokenSubscription: Subscription | null;

readonly user$ = this.loggedInUser$.asObservable();
readonly isTokenAllowed$ = new BehaviorSubject<boolean>(false);

/**
* Special case that only matches root and admin users.
Expand Down Expand Up @@ -83,6 +87,7 @@ export class AuthService {
) {
this.setupAuthenticationUpdate();
this.setupWsConnectionUpdate();
this.setupPeriodicTokenGeneration();
this.setupTokenUpdate();
}

Expand Down Expand Up @@ -205,10 +210,14 @@ export class AuthService {

this.wsStatus.setLoginStatus(true);
this.window.sessionStorage.setItem('loginBannerDismissed', 'true');
return this.authToken$.pipe(
take(1),
map(() => LoginResult.Success),
);
if (result?.authenticator === AuthenticatorLoginLevel.Level1) {
this.isTokenAllowed$.next(true);
return this.authToken$.pipe(
take(1),
map(() => LoginResult.Success),
);
}
return of(LoginResult.Success);
}
this.wsStatus.setLoginStatus(false);

Expand All @@ -221,14 +230,19 @@ export class AuthService {
}

private setupPeriodicTokenGeneration(): void {
if (!this.generateTokenSubscription || this.generateTokenSubscription.closed) {
this.generateTokenSubscription = timer(0, this.tokenRegenerationTimeMillis).pipe(
switchMap(() => this.wsStatus.isAuthenticated$.pipe(take(1))),
filter(Boolean),
switchMap(() => this.api.call('auth.generate_token')),
tap((token) => this.latestTokenGenerated$.next(token)),
).subscribe();
}
this.isTokenAllowed$.pipe(
filter(Boolean),
filterAsync(() => this.wsStatus.isAuthenticated$),
).subscribe(() => {
if (!this.generateTokenSubscription || this.generateTokenSubscription.closed) {
this.generateTokenSubscription = timer(0, this.tokenRegenerationTimeMillis).pipe(
switchMap(() => this.wsStatus.isAuthenticated$.pipe(take(1))),
filter(Boolean),
switchMap(() => this.api.call('auth.generate_token')),
tap((token) => this.latestTokenGenerated$.next(token)),
).subscribe();
}
});
}

private getLoggedInUserInformation(): Observable<LoggedInUser> {
Expand All @@ -240,11 +254,10 @@ export class AuthService {
}

private setupAuthenticationUpdate(): void {
this.wsStatus.isAuthenticated$.subscribe({
this.wsStatus.isAuthenticated$.pipe().subscribe({
next: (isAuthenticated) => {
if (isAuthenticated) {
this.store$.dispatch(adminUiInitialized());
this.setupPeriodicTokenGeneration();
} else if (this.generateTokenSubscription) {
this.latestTokenGenerated$?.complete();
this.latestTokenGenerated$ = new ReplaySubject<string>(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
[tooltip]="'Enable this mode to enhance system security to meet US federal government security requirements. Note that enabling STIG mode will restrict some functionality.' | translate"
></ix-slide-toggle>

<p class="hint">
{{ 'Restart is required after changing these settings.' | translate }}
</p>
<mat-hint>{{ 'Global Two-Factor Authentication must be enabled to activate this feature.' | translate }}</mat-hint>
<mat-hint>{{ 'Restart is required after changing these settings.' | translate }}</mat-hint>

<div class="form-actions">
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
}
}

.hint {
mat-hint {
display: flex;
font-size: 12px;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit,
ChangeDetectionStrategy, Component, OnInit,
signal,
} from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatHint } from '@angular/material/form-field';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Store } from '@ngrx/store';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
Expand All @@ -19,7 +20,6 @@ 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 { ErrorHandlerService } from 'app/services/error-handler.service';
import { AppState } from 'app/store';

@UntilDestroy()
@Component({
Expand All @@ -38,6 +38,7 @@ import { AppState } from 'app/store';
MatButton,
TestDirective,
TranslateModule,
MatHint,
],
})
export class SystemSecurityFormComponent implements OnInit {
Expand All @@ -48,14 +49,12 @@ export class SystemSecurityFormComponent implements OnInit {
enable_gpos_stig: [false],
});

private systemSecurityConfig: SystemSecurityConfig;
private systemSecurityConfig = signal<SystemSecurityConfig>(this.slideInRef.getData());

constructor(
private formBuilder: FormBuilder,
private cdr: ChangeDetectorRef,
private translate: TranslateService,
private snackbar: SnackbarService,
private store$: Store<AppState>,
private dialogService: DialogService,
private api: ApiService,
private errorHandler: ErrorHandlerService,
Expand All @@ -64,11 +63,10 @@ export class SystemSecurityFormComponent implements OnInit {
this.slideInRef.requireConfirmationWhen(() => {
return of(this.form.dirty);
});
this.systemSecurityConfig = this.slideInRef.getData();
}

ngOnInit(): void {
if (this.systemSecurityConfig) {
if (this.systemSecurityConfig()) {
this.initSystemSecurityForm();
}
}
Expand All @@ -92,7 +90,11 @@ export class SystemSecurityFormComponent implements OnInit {
}

private initSystemSecurityForm(): void {
this.form.patchValue(this.systemSecurityConfig);
this.cdr.markForCheck();
this.form.patchValue(this.systemSecurityConfig());
this.form.controls.enable_gpos_stig.valueChanges.pipe(untilDestroyed(this)).subscribe((value) => {
if (value && !this.form.controls.enable_fips.value) {
this.form.patchValue({ enable_fips: true });
}
});
}
}
5 changes: 5 additions & 0 deletions src/app/services/fips.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { of } from 'rxjs';
import { fakeSuccessfulJob } from 'app/core/testing/utils/fake-job.utils';
import { mockJob, mockApi } from 'app/core/testing/utils/mock-api.utils';
import { AuthService } from 'app/modules/auth/auth.service';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { ApiService } from 'app/modules/websocket/api.service';
import { FipsService } from 'app/services/fips.service';
Expand All @@ -26,6 +27,9 @@ describe('FipsService', () => {
mockApi([
mockJob('failover.reboot.other_node', fakeSuccessfulJob()),
]),
mockProvider(AuthService, {
clearAuthToken: jest.fn(),
}),
],
});

Expand All @@ -42,6 +46,7 @@ describe('FipsService', () => {
buttonText: 'Restart Now',
}),
);
expect(spectator.inject(AuthService).clearAuthToken).toHaveBeenCalled();
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/system-tasks/restart'], { skipLocationChange: true });
});
});
Expand Down
3 changes: 3 additions & 0 deletions src/app/services/fips.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Observable, of, switchMap } from 'rxjs';
import {
filter, tap,
} from 'rxjs/operators';
import { AuthService } from 'app/modules/auth/auth.service';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { ApiService } from 'app/modules/websocket/api.service';
Expand All @@ -27,6 +28,7 @@ export class FipsService {
private snackbar: SnackbarService,
private api: ApiService,
private errorHandler: ErrorHandlerService,
private authService: AuthService,
) {}

promptForRestart(): Observable<unknown> {
Expand All @@ -38,6 +40,7 @@ export class FipsService {
.pipe(
tap((approved) => {
if (approved) {
this.authService.clearAuthToken();
this.restart();
}
}),
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/af.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/be.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/br.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Global password to unlock SEDs.": "",
"Gmail": "",
"Go Back": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,7 @@
"Global Target Configuration": "",
"Global Two Factor Authentication": "",
"Global Two Factor Authentication Settings": "",
"Global Two-Factor Authentication must be enabled to activate this feature.": "",
"Gmail": "",
"Go Back": "",
"Go To Dataset": "",
Expand Down
Loading

0 comments on commit 12a16c1

Please sign in to comment.