Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stark-core): implement Stark XSRF module #708

Merged
merged 1 commit into from
Sep 26, 2018
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 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;
}
Loading