Skip to content

Commit

Permalink
feat(stark-core): implement Stark XSRF module
Browse files Browse the repository at this point in the history
ISSUES CLOSED: #115
  • Loading branch information
christophercr committed Sep 26, 2018
1 parent 8a787f7 commit e82ed0d
Show file tree
Hide file tree
Showing 17 changed files with 874 additions and 27 deletions.
1 change: 1 addition & 0 deletions packages/stark-core/src/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./modules/routing";
export * from "./modules/session";
export * from "./modules/settings";
export * from "./modules/user";
export * from "./modules/xsrf";
13 changes: 8 additions & 5 deletions packages/stark-core/src/modules/http/services/http.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/* tslint:disable:completed-docs*/
/**
* @ignore
*/
const _cloneDeep: Function = require("lodash/cloneDeep");
import { Deserialize, Serialize } from "cerialize";
import { Observable, throwError, timer } from "rxjs";
// FIXME Adapt mergeMap code --> See: https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md#howto-result-selector-migration
Expand All @@ -28,6 +24,11 @@ import {
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "../../logging/services";
import { STARK_SESSION_SERVICE, StarkSessionService } from "../../session/services";

/**
* @ignore
*/
const _cloneDeep: Function = require("lodash/cloneDeep");

/**
* @ignore
* Service to make HTTP calls in compliance with the guidelines from the NBB REST API Design Guide.
Expand All @@ -40,7 +41,9 @@ export class StarkHttpServiceImpl<P extends StarkResource> implements StarkHttpS
@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService,
@Inject(STARK_SESSION_SERVICE) private sessionService: StarkSessionService,
private httpClient: HttpClient
) {}
) {
this.logger.debug(starkHttpServiceName + " loaded");
}

public executeSingleItemRequest(request: StarkHttpRequest<P>): Observable<StarkSingleItemResponseWrapper<P>> {
// remove the etag before executing the request
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*tslint:disable:completed-docs*/
import Spy = jasmine.Spy;
import SpyObj = jasmine.SpyObj;
import { Injector } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable, of, throwError } from "rxjs";
import { Serialize } from "cerialize";
Expand All @@ -10,15 +11,17 @@ import { StarkLoggingServiceImpl } from "./logging.service";
import { StarkApplicationConfig, StarkApplicationConfigImpl } from "../../../configuration/entities/application";
import { StarkLogging, StarkLoggingImpl, StarkLogMessage, StarkLogMessageImpl, StarkLogMessageType } from "../../logging/entities";
import { StarkBackend } from "../../http/entities/backend";
import { StarkXSRFService } from "../../xsrf/services";
import { StarkCoreApplicationState } from "../../../common/store";
import { StarkError, StarkErrorImpl } from "../../../common/error";
import { MockStarkXsrfService } from "../../xsrf/testing/xsrf.mock";

// tslint:disable-next-line:no-big-function
describe("Service: StarkLoggingService", () => {
let appConfig: StarkApplicationConfig;
let mockStore: SpyObj<Store<StarkCoreApplicationState>>;
// FIXME: uncomment when XSRF service is implemented
// let mockXSRFService: StarkXSRFService;
let mockInjectorService: SpyObj<Injector>;
let mockXSRFService: StarkXSRFService;
let loggingService: LoggingServiceHelper;
const loggingBackend: StarkBackend = {
name: "logging",
Expand All @@ -39,22 +42,24 @@ describe("Service: StarkLoggingService", () => {

beforeEach(() => {
mockStore = jasmine.createSpyObj<Store<StarkCoreApplicationState>>("store", ["dispatch", "pipe"]);
mockInjectorService = jasmine.createSpyObj<Injector>("injector,", ["get"]);
appConfig = new StarkApplicationConfigImpl();
appConfig.debugLoggingEnabled = true;
appConfig.loggingFlushDisabled = false;
appConfig.loggingFlushApplicationId = "TEST";
appConfig.loggingFlushPersistSize = loggingFlushPersistSize;
appConfig.addBackend(loggingBackend);

// FIXME: uncomment when XSRF service is implemented
// mockXSRFService = UnitTestingUtils.getMockedXSRFService();
mockXSRFService = new MockStarkXsrfService();
mockStarkLogging = {
uuid: "dummy uuid",
applicationId: "dummy app id",
messages: []
};
mockStore.pipe.and.returnValue(of(mockStarkLogging));
loggingService = new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/);
/* tslint:disable-next-line:deprecation */
(<Spy>mockInjectorService.get).and.returnValue(mockXSRFService);
loggingService = new LoggingServiceHelper(mockStore, appConfig, mockInjectorService);
// reset the calls counter because there is a log in the constructor
mockStore.dispatch.calls.reset();
});
Expand All @@ -66,15 +71,15 @@ describe("Service: StarkLoggingService", () => {
for (const invalidValue of invalidValues) {
appConfig.loggingFlushPersistSize = invalidValue;

expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/loggingFlushPersistSize/);
expect(() => new LoggingServiceHelper(mockStore, appConfig, mockInjectorService)).toThrowError(/loggingFlushPersistSize/);
}
});

it("should throw an error in case the logging flushing is enabled but the backend config is missing", () => {
appConfig.loggingFlushDisabled = false;
appConfig.backends.delete("logging");

expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/backend/);
expect(() => new LoggingServiceHelper(mockStore, appConfig, mockInjectorService)).toThrowError(/backend/);
});

it("should generate a new correlation id", () => {
Expand Down Expand Up @@ -308,8 +313,8 @@ describe("Service: StarkLoggingService", () => {
});

class LoggingServiceHelper extends StarkLoggingServiceImpl {
public constructor(store: Store<StarkCoreApplicationState>, appConfig: StarkApplicationConfig /*, xsrfService: StarkXSRFService*/) {
super(store, appConfig /*, xsrfService*/);
public constructor(store: Store<StarkCoreApplicationState>, appConfig: StarkApplicationConfig, injector: Injector) {
super(store, appConfig, injector);
}

public constructLogMessageHelper(messageType: StarkLogMessageType, ...args: any[]): StarkLogMessage {
Expand Down
40 changes: 30 additions & 10 deletions packages/stark-core/src/modules/logging/services/logging.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
/* tslint:disable:completed-docs*/
import uuid from "uuid";

import { Serialize } from "cerialize";

import { select, Store } from "@ngrx/store";
import { Inject, Injectable, Injector } from "@angular/core";
import { Observable, Subject } from "rxjs";

import { Inject, Injectable } from "@angular/core";

import { StarkLoggingService, starkLoggingServiceName } from "./logging.service.intf";
import { STARK_APP_CONFIG, StarkApplicationConfig } from "../../../configuration/entities/application";
import { StarkBackend } from "../../http/entities/backend";
import { StarkCoreApplicationState } from "../../../common/store";
import { StarkHttpStatusCodes } from "../../http/enumerators";
import { StarkHttpHeaders } from "../../http/constants";
import { STARK_XSRF_SERVICE, StarkXSRFService } from "../../xsrf/services/xsrf.service.intf";
import { StarkLogging, StarkLoggingImpl, StarkLogMessage, StarkLogMessageImpl, StarkLogMessageType } from "../entities";
import { StarkFlushLogMessages, StarkLogMessageAction } from "../actions";
import { selectStarkLogging } from "../reducers";
Expand All @@ -24,6 +22,8 @@ import { StarkConfigurationUtil } from "../../../util/configuration.util";
*/
const _noop: Function = require("lodash/noop");

const xsrfServiceNotFound: "not provided" = "not provided";

/**
* @ignore
* @ngdoc service
Expand All @@ -43,14 +43,14 @@ export class StarkLoggingServiceImpl implements StarkLoggingService {
private consoleError: Function;
private starkLogging: StarkLogging;
/** @internal */
private _xsrfService?: StarkXSRFService | typeof xsrfServiceNotFound;
/** @internal */
private _correlationId: string;

// FIXME: uncomment these lines once XSRF Service is implemented
public constructor(
private store: Store<StarkCoreApplicationState>,
@Inject(STARK_APP_CONFIG)
private appConfig: StarkApplicationConfig /*,
@Inject(starkXSRFServiceName) private xsrfService: StarkXSRFService*/
@Inject(STARK_APP_CONFIG) private appConfig: StarkApplicationConfig,
private injector: Injector
) {
// ensuring that the app config is valid before doing anything
StarkConfigurationUtil.validateConfig(this.appConfig, ["logging", "http"], starkLoggingServiceName);
Expand Down Expand Up @@ -214,8 +214,9 @@ export class StarkLoggingServiceImpl implements StarkLoggingService {
// IE "Access is denied" error: https://stackoverflow.com/questions/22098259/access-denied-in-ie-10-and-11-when-ajax-target-is-localhost
try {
xhr.open("POST", url, async);
// FIXME: uncomment when XSRF service is implemented
// this.xsrfService.configureXHR(xhr);
if (this.xsrfService) {
this.xsrfService.configureXHR(xhr);
}
xhr.setRequestHeader(StarkHttpHeaders.CONTENT_TYPE, "application/json");
xhr.send(serializedData);
} catch (e) {
Expand Down Expand Up @@ -259,4 +260,23 @@ export class StarkLoggingServiceImpl implements StarkLoggingService {
return logFn.apply(console, consoleArgs);
};
}

/**
* Gets the StarkXSRFService from the Injector (this is tried only once).
* It returns 'undefined' if the service is not found (the XSRF module is not imported in the app).
*/
private get xsrfService(): StarkXSRFService | undefined {
if (typeof this._xsrfService === "undefined") {
// The StarkXSRFService should be resolved at runtime to prevent the Angular DI circular dependency errors
try {
this._xsrfService = this.injector.get<StarkXSRFService>(STARK_XSRF_SERVICE);
return this._xsrfService;
} catch (exception) {
this._xsrfService = xsrfServiceNotFound;
return undefined;
}
}

return this._xsrfService !== xsrfServiceNotFound ? this._xsrfService : undefined;
}
}
3 changes: 3 additions & 0 deletions packages/stark-core/src/modules/xsrf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./xsrf/interceptors";
export * from "./xsrf/services";
export * from "./xsrf/xsrf.module";
1 change: 1 addition & 0 deletions packages/stark-core/src/modules/xsrf/interceptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./interceptors/http-xsrf.interceptor";
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Inject, Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { StarkXSRFService, STARK_XSRF_SERVICE } from "../services/xsrf.service.intf";

/**
* Angular Http interceptor that adds the XSRF configuration to every state-changing request (POST,PUT,PATCH and DELETE)
* and stores the XSRF token from every response.
*
* Defined in the HttpClientXsrfModule set in packages/stark-core/src/modules/http/http.module.ts
*/
@Injectable()
export class StarkXSRFHttpInterceptor implements HttpInterceptor {
public constructor(@Inject(STARK_XSRF_SERVICE) public xsrfService: StarkXSRFService) {}

/**
* @param request - The intercepted outgoing `HttpRequest`
* @param next - The next request handler where the `HttpRequest` will be forwarded to
* @returns The modified `HttpRequest` with the XSRF configuration enabled.
*/
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const xsrfProtectedRequest: HttpRequest<any> = this.xsrfService.configureHttpRequest(request);

return next
.handle(xsrfProtectedRequest) // pass request through to the next request handler
.pipe(
// the Http response is intercepted in order to extract and store the XSRF token via the XSRF service
tap((_httpResponse: HttpEvent<any>) => {
this.xsrfService.storeXSRFToken();
})
);
}
}
3 changes: 3 additions & 0 deletions packages/stark-core/src/modules/xsrf/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./services/xsrf.service";
export * from "./services/xsrf-config.intf";
export { STARK_XSRF_SERVICE, StarkXSRFService } from "./services/xsrf.service.intf";
35 changes: 35 additions & 0 deletions packages/stark-core/src/modules/xsrf/services/xsrf-config.intf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { InjectionToken } from "@angular/core";
import { Observable } from "rxjs";

/**
* The InjectionToken version of the config name
*/
export const STARK_XSRF_CONFIG: InjectionToken<StarkXSRFConfig> = new InjectionToken<StarkXSRFConfig>("StarkXSRFConfig");

/**
* Alternative literal object to define the waitBeforePinging function and its DI dependencies
*/
export interface StarkXSRFWaitBeforePingingLiteral {
/**
* Array of Dependency Injection tokens for the dependencies of the waitBeforePingingFn.
*/
deps: any[];

/**
* Function that will be called by the XSRF service passing the necessary dependencies to get the corresponding Promise/Observable
* that the service should wait for before pinging all the backends.
*/
waitBeforePingingFn: (...deps: any[]) => Promise<any> | PromiseLike<any> | Observable<any>;
}

/**
* Definition of the configuration object for the Stark XSRF service
*/
export interface StarkXSRFConfig {
/**
* Function that will be called by the XSRF service to get the corresponding Promise/Observable
* that the service should wait for before pinging all the backends.
* Alternatively, this can be defined as a {@link StarkXSRFWaitBeforePingingLiteral|literal}
*/
waitBeforePinging?: (() => Promise<any> | PromiseLike<any> | Observable<any>) | StarkXSRFWaitBeforePingingLiteral;
}
52 changes: 52 additions & 0 deletions packages/stark-core/src/modules/xsrf/services/xsrf.service.intf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { InjectionToken } from "@angular/core";
import { HttpRequest } from "@angular/common/http";

/**
* The name of the service in case an injection is needed
*/
export const starkXSRFServiceName: string = "StarkXSRFService";
/**
* The InjectionToken version of the service name
*/
export const STARK_XSRF_SERVICE: InjectionToken<StarkXSRFService> = new InjectionToken<StarkXSRFService>(starkXSRFServiceName);

/**
* Stark XSRF Service.
* Service to get/store the XSRF token to be used with the different backends.
*/
export interface StarkXSRFService {
/**
* Add the necessary options to the XHR config in order to enable XSRF protection.
* Since the service will add the XSRF header to the XHR object, this method must be called after calling the XHR open() method because
* headers cannot be set before open(). See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
* This method should be used for those HTTP state-changing requests (POST, PUT, PATCH or DELETE) which are not performed
* using StarkHttpService or Angular raw $http
* @param xhr - The XHR object to be configured
*/
configureXHR(xhr: XMLHttpRequest): void;

/**
* Return a new `HttpRequest` including the necessary options for state-changing requests (POST, PUT, PATCH or DELETE)
* in order to enable XSRF protection.
* Logs a warning whenever there is no XSRF token to be sent in such requests
* @param request - The Angular `HttpRequest` to be modified
* @returns The modified Angular `HttpRequest`
*/
configureHttpRequest(request: HttpRequest<any>): HttpRequest<any>;

/**
* Get the current XSRF token (in case there is one already stored)
*/
getXSRFToken(): string | undefined;

/**
* Store the token from the current XSRF cookie
*/
storeXSRFToken(): void;

/**
* Trigger a GET Http request to all the backends in order to get their XSRF tokens.
* Then the response is intercepted by the XSRF Http Interceptor to store the token from the current XSRF cookie
*/
pingBackends(): void;
}

0 comments on commit e82ed0d

Please sign in to comment.