diff --git a/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ProtectedFeatureController.cs b/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ProtectedFeatureController.cs index b967600586..7e7f2d8c14 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ProtectedFeatureController.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ProtectedFeatureController.cs @@ -12,8 +12,7 @@ using CSETWebCore.DataLayer.Model; using CSETWebCore.Model.Module; using CSETWebCore.Interfaces.Helpers; -using CSETWebCore.Helpers; -using Namotion.Reflection; +using Microsoft.AspNetCore.Authorization; namespace CSETWebCore.Api.Controllers { @@ -116,9 +115,10 @@ public IActionResult SetCisaAssessorWorkflow([FromBody] bool cisaWorkflowEnabled } [HttpGet] + [AllowAnonymous] [Route("api/EnableProtectedFeature/getCisaAssessorWorkflow")] /// - /// Marks the FAA set as 'unlocked.' + /// Gets the status of the CISA assessor workflow for the current user. /// /// public IActionResult GetCisaAssessorWorkflow() @@ -126,6 +126,7 @@ public IActionResult GetCisaAssessorWorkflow() var userId = _tokenManager.GetCurrentUserId(); var ak = _tokenManager.GetAccessKey(); + // Assume false if we can't find the user or access key (they are probably on the login page). bool cisaWorkflowEnabled = false; if (userId != null) diff --git a/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.html b/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.html index 0719705522..2bbb868ead 100644 --- a/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.html +++ b/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.html @@ -38,7 +38,7 @@ -
+
diff --git a/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.ts b/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.ts index 233fc094e0..19cc7439fa 100644 --- a/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.ts +++ b/CSETWebNg/src/app/dialogs/enable-protected/enable-protected.component.ts @@ -39,6 +39,7 @@ export class EnableProtectedComponent implements OnInit { message: any; enableFeatureButtonClick: boolean = false; cisaWorkflowEnabled: boolean = false; + cisaWorkflowStatusLoaded: boolean = false; constructor(private dialog: MatDialogRef, private featureSvc: EnableFeatureService, @@ -54,6 +55,7 @@ export class EnableProtectedComponent implements OnInit { this.configSvc.getCisaAssessorWorkflow().subscribe((cisaWorkflowEnabled: boolean) => { this.cisaWorkflowEnabled = cisaWorkflowEnabled; + this.cisaWorkflowStatusLoaded = true; }); } diff --git a/CSETWebNg/src/app/services/authentication.service.ts b/CSETWebNg/src/app/services/authentication.service.ts index 7d4ce48f85..198abe4f80 100644 --- a/CSETWebNg/src/app/services/authentication.service.ts +++ b/CSETWebNg/src/app/services/authentication.service.ts @@ -28,7 +28,6 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; - import { JwtParser } from '../helpers/jwt-parser'; import { ChangePassword } from '../models/reset-pass.model'; import { CreateUser } from './../models/user.model'; @@ -36,332 +35,360 @@ import { ConfigService } from './config.service'; import { environment } from '../../environments/environment'; export interface LoginResponse { - token: string; - resetRequired: boolean; - isPasswordExpired: boolean; - isSuperUser: boolean; - userLastName: string; - userFirstName: string; - userId?: number; - email: string; - accessKey?: string; - exportExtension: string; - importExtensions: string; - linkerTime: string; + token: string; + resetRequired: boolean; + isPasswordExpired: boolean; + isSuperUser: boolean; + userLastName: string; + userFirstName: string; + userId?: number; + email: string; + accessKey?: string; + exportExtension: string; + importExtensions: string; + linkerTime: string; } const headers = { - headers: new HttpHeaders() - .set('Content-Type', 'application/json'), - params: new HttpParams() + headers: new HttpHeaders().set('Content-Type', 'application/json'), + params: new HttpParams() }; @Injectable() export class AuthenticationService { - isLocal: boolean; - private initialized = false; - private parser = new JwtParser(); - - isAuthenticated = false; - - constructor( - private http: HttpClient, - private router: Router, - private configSvc: ConfigService, - public dialog: MatDialog - ) { - if (!this.initialized) { - this.initialized = true; - } - } - - /** - * Indicates if the user has agreed to the privacy warning. - * Returns true if not running as "CSET Online". - * - * Only CSET Online requires the user agreement. - * If not currently running as CSET Online, always returns true. - */ - hasUserAgreedToPrivacyWarning() { - if (!this.configSvc.config.behaviors.showPrivacyWarning) { - return true; - } - - return (sessionStorage.getItem('hasUserAgreedToPrivacyWarning') == 'true'); + isLocal: boolean; + private initialized = false; + private parser = new JwtParser(); + + isAuthenticated = false; + + constructor( + private http: HttpClient, + private router: Router, + private configSvc: ConfigService, + public dialog: MatDialog + ) { + if (!this.initialized) { + this.initialized = true; } - - /** - * - */ - checkLocal() { - return this.http.post(this.configSvc.apiUrl + 'auth/login/standalone', - JSON.stringify( - { - TzOffset: new Date().getTimezoneOffset().toString(), - // If InstallationMode isn't empty, use it. Otherwise default to environment.appCode - Scope: (this.configSvc.installationMode || '') !== '' ? this.configSvc.installationMode : environment.appCode - } - ), headers) - .toPromise().then( - (response: LoginResponse) => { - - if (response?.email === null || response?.email === undefined) { - this.isLocal = false; - } else { - this.isLocal = true; - this.storeUserData(response); - } - - localStorage.setItem('cset.isLocal', (this.isLocal + '')); - - localStorage.setItem('cset.linkerDate', response?.linkerTime); - }, - error => { - console.warn('Error getting stand-alone status. Assuming non-stand-alone mode.'); - this.isLocal = false; - }); + } + + /** + * Indicates if the user has agreed to the privacy warning. + * Returns true if not running as "CSET Online". + * + * Only CSET Online requires the user agreement. + * If not currently running as CSET Online, always returns true. + */ + hasUserAgreedToPrivacyWarning() { + if (!this.configSvc.config.behaviors.showPrivacyWarning) { + return true; } - /** - * Calls the API to find out whether this is a local install - */ - checkLocalInstallStatus() { - return this.http.get(this.configSvc.apiUrl + 'auth/islocal', headers); - } - - /** - * - * @param user - */ - storeUserData(user: LoginResponse) { - if (user.token != null) { - localStorage.setItem('userToken', user.token); - } - localStorage.setItem('firstName', user.userFirstName); - localStorage.setItem('lastName', user.userLastName); - localStorage.setItem('superUser', '' + user.isSuperUser); - localStorage.setItem('userId', '' + user.userId); - localStorage.setItem('email', user.email); - localStorage.setItem('exportExtension', user.exportExtension); - localStorage.setItem('importExtensions', user.importExtensions) - localStorage.setItem('developer', String(false)); - - - // schedule the first token refresh event - this.scheduleTokenRefresh(this.http, user.token); - } - - /** - * - * @param email - * @param password - */ - login(email: string, password: string) { - localStorage.clear(); - localStorage.setItem('email', email); - - // set the scope (application) - let scope: string; - - switch (this.configSvc.installationMode || '') { - case 'ACET': - scope = 'ACET'; - break; - case 'TSA': - scope = 'TSA'; - break; - case 'RRA': - scope = 'RRA'; - break; - case 'CF': - scope = 'CF'; - break; - case 'IOD': - scope = 'IOD'; - break; - default: - scope = environment.appCode + return sessionStorage.getItem('hasUserAgreedToPrivacyWarning') == 'true'; + } + + /** + * + */ + checkLocal() { + return this.http + .post( + this.configSvc.apiUrl + 'auth/login/standalone', + JSON.stringify({ + TzOffset: new Date().getTimezoneOffset().toString(), + // If InstallationMode isn't empty, use it. Otherwise default to environment.appCode + Scope: (this.configSvc.installationMode || '') !== '' ? this.configSvc.installationMode : environment.appCode + }), + headers + ) + .toPromise() + .then( + (response: LoginResponse) => { + if (response?.email === null || response?.email === undefined) { + this.isLocal = false; + } else { + this.isLocal = true; + this.storeUserData(response); + } + + localStorage.setItem('cset.isLocal', this.isLocal + ''); + + localStorage.setItem('cset.linkerDate', response?.linkerTime); + }, + (error) => { + console.warn('Error getting stand-alone status. Assuming non-stand-alone mode.'); + this.isLocal = false; } - - return this.http.post(this.configSvc.apiUrl + 'auth/login', - JSON.stringify( - { - Email: email, - Password: password, - TzOffset: new Date().getTimezoneOffset().toString(), - Scope: scope - } - ), headers).pipe( - map((user: LoginResponse) => { - // store user details and jwt token in local storage to keep user logged in between page refreshes - this.storeUserData(user); - - this.isAuthenticated = true; - - return user; - })); - } - - - logout() { - this.isAuthenticated = false; - this.router.navigate(['/home/logout'], { queryParamsHandling: "preserve" }); - } - - /** - * TODO: This is not working correctly - the local storage stuff - * hangs around even if we are sitting on the login page again - */ - // isAuthenticated() { - // const uid = localStorage.getItem('userId'); - // return !!uid; - // } - - - /** - * Schedules an HTTP transaction to refresh the JWT. - * @param http The current HttpClient instance. - * @param token A JWT string. - */ - scheduleTokenRefresh(http: HttpClient, token: string) { - const refresh = timer(this.calcTokenRefreshTimeout(token)); - refresh.subscribe( - val => { - // only schedule a refresh if the user is currently logged on - if (localStorage.getItem('userToken') != null) { - - http.get(this.configSvc.apiUrl + 'auth/token?refresh') - .subscribe((resp: LoginResponse) => { - localStorage.removeItem('userToken'); - localStorage.setItem('userToken', resp.token); - - // schedule the next refresh - this.scheduleTokenRefresh(this.http, resp.token); - }, error => { - console.log(error.message); - }); - } - }); - } - - - /** - * Returns the timeout interval in milliseconds - * @param token A JWT string. - */ - calcTokenRefreshTimeout(token: string): number { - // extract the expiration timestamp from the token - const jwt = new JwtParser(); - const parsedToken = jwt.decodeToken(token); - const expTimeUnix = parsedToken.exp; - - const nowUtcUnix = Math.floor((new Date()).getTime() / 1000); - - // how many seconds from now until expiry? - const secondsUntilExpiration = expTimeUnix - nowUtcUnix; - - // refresh at 60 seconds lead time before expiry - const leadSeconds = 60; - const refreshIntervalMs = (secondsUntilExpiration - leadSeconds) * 1000; - - return refreshIntervalMs; - } - - /** - * Requests a JWT with a short lifespan. - */ - getShortLivedToken() { - return this.http.get(this.configSvc.apiUrl + 'auth/token?expSeconds=30000'); - } - - getShortLivedTokenForAssessment(assessment_id: number) { - return this.http.get(this.configSvc.apiUrl + 'auth/token?assessmentId=' + assessment_id + '&expSeconds=30000'); - } - - changePassword(data: ChangePassword) { - return this.http.post(this.configSvc.apiUrl + 'ResetPassword/ChangePassword', JSON.stringify(data), { 'headers': headers.headers, params: headers.params, responseType: 'text' }); - } - - checkPassword(data: ChangePassword): Observable { - return this.http.post(this.configSvc.apiUrl + 'ResetPassword/CheckPassword', JSON.stringify(data), { 'headers': headers.headers, params: headers.params, responseType: 'text' }); - } - - updateUser(data: CreateUser): Observable { - return this.http.post(this.configSvc.apiUrl + 'contacts/UpdateUser', data, headers); - } - - getUserInfo(): Observable { - return this.http.get(this.configSvc.apiUrl + 'contacts/GetUserInfo'); - } - - passwordStatus() { - return this.http.get(this.configSvc.apiUrl + 'ResetPassword/ResetPasswordStatus/', headers); - } - - getSecurityQuestionsList(email: string) { - return this.http.get(this.configSvc.apiUrl + 'ResetPassword/SecurityQuestions?email=' + email + '&appCode=' + environment.appCode); - } - - getSecurityQuestionsPotentialList() { - return this.http.get(this.configSvc.apiUrl + 'ResetPassword/PotentialQuestions'); - } - - userToken() { - return localStorage.getItem('userToken'); + ); + } + + /** + * Calls the API to find out whether this is a local install + */ + checkLocalInstallStatus() { + return this.http.get(this.configSvc.apiUrl + 'auth/islocal', headers); + } + + /** + * + * @param user + */ + storeUserData(user: LoginResponse) { + if (user.token != null) { + localStorage.setItem('userToken', user.token); } - - userId(): number { - return parseInt(localStorage.getItem('userId'), 10); - } - - accessKey(): string { - return localStorage.getItem('accessKey'); - } - - email() { - return localStorage.getItem('email'); - } - - firstName() { - return localStorage.getItem('firstName'); + localStorage.setItem('firstName', user.userFirstName); + localStorage.setItem('lastName', user.userLastName); + localStorage.setItem('superUser', '' + user.isSuperUser); + localStorage.setItem('userId', '' + user.userId); + localStorage.setItem('email', user.email); + localStorage.setItem('exportExtension', user.exportExtension); + localStorage.setItem('importExtensions', user.importExtensions); + localStorage.setItem('developer', String(false)); + + // schedule the first token refresh event + this.scheduleTokenRefresh(this.http, user.token); + } + + /** + * + * @param email + * @param password + */ + login(email: string, password: string) { + localStorage.clear(); + localStorage.setItem('email', email); + + // set the scope (application) + let scope: string; + + switch (this.configSvc.installationMode || '') { + case 'ACET': + scope = 'ACET'; + break; + case 'TSA': + scope = 'TSA'; + break; + case 'RRA': + scope = 'RRA'; + break; + case 'CF': + scope = 'CF'; + break; + case 'IOD': + scope = 'IOD'; + break; + default: + scope = environment.appCode; } - lastName() { - return localStorage.getItem('lastName'); - } - - setUserInfo(info: CreateUser) { - localStorage.setItem('firstName', info.firstName); - localStorage.setItem('lastName', info.lastName); - localStorage.setItem('email', info.primaryEmail); - } - - /** - * - */ - loginWithAccessKey(loginKey) { - const req = JSON.stringify( - { - accessKey: loginKey, - tzOffset: new Date().getTimezoneOffset().toString(), - Scope: 'CSET' - } - ) - - return this.http.post(this.configSvc.apiUrl + 'auth/login/accesskey', req, headers) - .pipe( - map((user: LoginResponse) => { - // store user details and jwt token in local storage to keep user logged in between page refreshes - this.storeUserData(user); - - return user; - }));; - } - - /** - * - */ - generateAccessKey() { - return this.http.get(this.configSvc.apiUrl + 'auth/accesskey', { responseType: 'text' }); - } + return this.http + .post( + this.configSvc.apiUrl + 'auth/login', + JSON.stringify({ + Email: email, + Password: password, + TzOffset: new Date().getTimezoneOffset().toString(), + Scope: scope + }), + headers + ) + .pipe( + map((user: LoginResponse) => { + // store user details and jwt token in local storage to keep user logged in between page refreshes + this.storeUserData(user); + + this.isAuthenticated = true; + + return this.configureCisaAssessorWorkflow(user); + }) + ); + } + + logout() { + this.isAuthenticated = false; + this.router.navigate(['/home/logout'], { queryParamsHandling: 'preserve' }); + } + + /** + * TODO: This is not working correctly - the local storage stuff + * hangs around even if we are sitting on the login page again + */ + // isAuthenticated() { + // const uid = localStorage.getItem('userId'); + // return !!uid; + // } + + /** + * Schedules an HTTP transaction to refresh the JWT. + * @param http The current HttpClient instance. + * @param token A JWT string. + */ + scheduleTokenRefresh(http: HttpClient, token: string) { + const refresh = timer(this.calcTokenRefreshTimeout(token)); + refresh.subscribe((val) => { + // only schedule a refresh if the user is currently logged on + if (localStorage.getItem('userToken') != null) { + http.get(this.configSvc.apiUrl + 'auth/token?refresh').subscribe( + (resp: LoginResponse) => { + localStorage.removeItem('userToken'); + localStorage.setItem('userToken', resp.token); + + // schedule the next refresh + this.scheduleTokenRefresh(this.http, resp.token); + }, + (error) => { + console.log(error.message); + } + ); + } + }); + } + + /** + * Returns the timeout interval in milliseconds + * @param token A JWT string. + */ + calcTokenRefreshTimeout(token: string): number { + // extract the expiration timestamp from the token + const jwt = new JwtParser(); + const parsedToken = jwt.decodeToken(token); + const expTimeUnix = parsedToken.exp; + + const nowUtcUnix = Math.floor(new Date().getTime() / 1000); + + // how many seconds from now until expiry? + const secondsUntilExpiration = expTimeUnix - nowUtcUnix; + + // refresh at 60 seconds lead time before expiry + const leadSeconds = 60; + const refreshIntervalMs = (secondsUntilExpiration - leadSeconds) * 1000; + + return refreshIntervalMs; + } + + /** + * Requests a JWT with a short lifespan. + */ + getShortLivedToken() { + return this.http.get(this.configSvc.apiUrl + 'auth/token?expSeconds=30000'); + } + + getShortLivedTokenForAssessment(assessment_id: number) { + return this.http.get(this.configSvc.apiUrl + 'auth/token?assessmentId=' + assessment_id + '&expSeconds=30000'); + } + + changePassword(data: ChangePassword) { + return this.http.post(this.configSvc.apiUrl + 'ResetPassword/ChangePassword', JSON.stringify(data), { + headers: headers.headers, + params: headers.params, + responseType: 'text' + }); + } + + checkPassword(data: ChangePassword): Observable { + return this.http.post(this.configSvc.apiUrl + 'ResetPassword/CheckPassword', JSON.stringify(data), { + headers: headers.headers, + params: headers.params, + responseType: 'text' + }); + } + + updateUser(data: CreateUser): Observable { + return this.http.post(this.configSvc.apiUrl + 'contacts/UpdateUser', data, headers); + } + + getUserInfo(): Observable { + return this.http.get(this.configSvc.apiUrl + 'contacts/GetUserInfo'); + } + + passwordStatus() { + return this.http.get(this.configSvc.apiUrl + 'ResetPassword/ResetPasswordStatus/', headers); + } + + getSecurityQuestionsList(email: string) { + return this.http.get( + this.configSvc.apiUrl + 'ResetPassword/SecurityQuestions?email=' + email + '&appCode=' + environment.appCode + ); + } + + getSecurityQuestionsPotentialList() { + return this.http.get(this.configSvc.apiUrl + 'ResetPassword/PotentialQuestions'); + } + + userToken() { + return localStorage.getItem('userToken'); + } + + userId(): number { + return parseInt(localStorage.getItem('userId'), 10); + } + + accessKey(): string { + return localStorage.getItem('accessKey'); + } + + email() { + return localStorage.getItem('email'); + } + + firstName() { + return localStorage.getItem('firstName'); + } + + lastName() { + return localStorage.getItem('lastName'); + } + + setUserInfo(info: CreateUser) { + localStorage.setItem('firstName', info.firstName); + localStorage.setItem('lastName', info.lastName); + localStorage.setItem('email', info.primaryEmail); + } + + /** + * Checks and sets the current user's cisa assessor workflow option. + */ + configureCisaAssessorWorkflow(user) { + return this.configSvc + .getCisaAssessorWorkflow() + .toPromise() + .then((cisaAssessorWorkflowEnabled) => { + if (cisaAssessorWorkflowEnabled) { + return this.configSvc.enableCisaAssessorWorkflow().then(() => { + return user; + }); + } else { + return user; + } + }); + } + + /** + * + */ + loginWithAccessKey(loginKey) { + const req = JSON.stringify({ + accessKey: loginKey, + tzOffset: new Date().getTimezoneOffset().toString(), + Scope: 'CSET' + }); + + return this.http.post(this.configSvc.apiUrl + 'auth/login/accesskey', req, headers).pipe( + map((user: LoginResponse) => { + // store user details and jwt token in local storage to keep user logged in between page refreshes + this.storeUserData(user); + + return this.configureCisaAssessorWorkflow(user); + }) + ); + } + + /** + * + */ + generateAccessKey() { + return this.http.get(this.configSvc.apiUrl + 'auth/accesskey', { responseType: 'text' }); + } } diff --git a/CSETWebNg/src/app/services/config.service.ts b/CSETWebNg/src/app/services/config.service.ts index 180ffa81e9..e543f6556a 100644 --- a/CSETWebNg/src/app/services/config.service.ts +++ b/CSETWebNg/src/app/services/config.service.ts @@ -104,15 +104,9 @@ export class ConfigService { .toPromise() .then((cisaAssessorWorkflowEnabled) => { if (cisaAssessorWorkflowEnabled) { - return this.http - .get('assets/settings/config.IOD.json') - .toPromise() - .then((iodConfig) => { - merge(this.config, iodConfig); - localStorage.setItem('installationMode', this.config.installationMode.toUpperCase()); - this.setConfigPropertiesForLocalService(); - }); + return this.enableCisaAssessorWorkflow() } + localStorage.setItem('installationMode', this.config.installationMode.toUpperCase()); }); }); @@ -123,6 +117,17 @@ export class ConfigService { } } + enableCisaAssessorWorkflow() { + return this.http + .get('assets/settings/config.IOD.json') + .toPromise() + .then((iodConfig) => { + merge(this.config, iodConfig); + localStorage.setItem('installationMode', this.config.installationMode.toUpperCase()); + this.setConfigPropertiesForLocalService(); + }); + } + /** * */ @@ -225,10 +230,7 @@ export class ConfigService { } setCisaAssessorWorkflow(cisaAssessorWorkflowEnabled: boolean) { - return this.http.post( - this.apiUrl + 'EnableProtectedFeature/setCisaAssessorWorkflow', - cisaAssessorWorkflowEnabled - ); + return this.http.post(this.apiUrl + 'EnableProtectedFeature/setCisaAssessorWorkflow', cisaAssessorWorkflowEnabled); } switchConfigsForMode(installationMode) {