[MNT-24614] Fixed APS basic auth login issue with ADF (#10364)

* [MNT-24614] Fixed APS basic auth login issue with ADF

* [MNT-24614] Addressed code review findings - Using includes api, and removed unneeded functions. Added missing return type to functions

* [MNT-24614] Added unit tests

* [MNT-24614] Added unit tests

* [MNT-24614] Fixed casing of unit test titles
This commit is contained in:
swapnil-verma-gl
2024-11-08 14:06:40 +05:30
committed by GitHub
parent 3ec3e732c0
commit 35c8093706
4 changed files with 180 additions and 89 deletions

View File

@@ -387,4 +387,36 @@ describe('AdfHttpClient', () => {
req.flush(null, { status: 200, statusText: 'Ok' });
});
it('should set X-CSRF-TOKEN header if CSRF is enabled', () => {
const options: RequestOptions = {
path: '',
httpMethod: 'GET'
};
angularHttpClient.disableCsrf = false;
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((error) => fail(error));
const req = controller.expectOne('http://example.com');
expect(req.request.headers.get('X-CSRF-TOKEN')).toBeDefined();
req.flush(null, { status: 200, statusText: 'Ok' });
});
it('should not set X-CSRF-TOKEN header if CSRF is disabled', () => {
const options: RequestOptions = {
path: '',
httpMethod: 'GET'
};
angularHttpClient.disableCsrf = true;
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((error) => fail(error));
const req = controller.expectOne('http://example.com');
expect(req.request.headers.get('X-CSRF-TOKEN')).toBeNull();
req.flush(null, { status: 200, statusText: 'Ok' });
});
});

View File

@@ -17,15 +17,7 @@
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api';
import {
HttpClient,
HttpContext,
HttpErrorResponse,
HttpEvent,
HttpHeaders,
HttpParams,
HttpResponse
} from '@angular/common/http';
import { HttpClient, HttpContext, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of, Subject, throwError } from 'rxjs';
import { catchError, map, takeUntil } from 'rxjs/operators';
@@ -52,8 +44,7 @@ export interface Emitters {
@Injectable({
providedIn: 'root'
})
export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
export class AdfHttpClient implements ee.Emitter, JsApiHttpClient {
on: ee.EmitterMethod;
off: ee.EmitterMethod;
once: ee.EmitterMethod;
@@ -107,47 +98,43 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder());
const responseType = AdfHttpClient.getResponseType(options);
const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true);
const security: SecurityOptions = {...this.defaultSecurityOptions, ...sc};
const security: SecurityOptions = { ...this.defaultSecurityOptions, ...sc };
const headers = this.getHeaders(options);
if (!emitters) {
emitters = this.getEventEmitters();
}
const request = this.httpClient.request(
options.httpMethod,
url,
{
context,
...(body && {body}),
...(responseType && {responseType}),
...security,
...(params && {params}),
headers,
observe: 'events',
reportProgress: true
}
);
const request = this.httpClient.request(options.httpMethod, url, {
context,
...(body && { body }),
...(responseType && { responseType }),
...security,
...(params && { params }),
headers,
observe: 'events',
reportProgress: true
});
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
}
post<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
return this.request<T>(url, {...options, httpMethod: 'POST'}, sc, emitters);
return this.request<T>(url, { ...options, httpMethod: 'POST' }, sc, emitters);
}
put<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
return this.request<T>(url, {...options, httpMethod: 'PUT'}, sc, emitters);
return this.request<T>(url, { ...options, httpMethod: 'PUT' }, sc, emitters);
}
get<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
return this.request<T>(url, {...options, httpMethod: 'GET'}, sc, emitters);
return this.request<T>(url, { ...options, httpMethod: 'GET' }, sc, emitters);
}
delete<T = void>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
return this.request<T>(url, {...options, httpMethod: 'DELETE'}, sc, emitters);
return this.request<T>(url, { ...options, httpMethod: 'DELETE' }, sc, emitters);
}
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
const eventPromise = Object.assign(promise, {
on() {
// eslint-disable-next-line prefer-spread, prefer-rest-params
@@ -189,58 +176,59 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
}
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
const abort$ = new Subject<void>();
const {eventEmitter, apiClientEmitter} = emitters;
const { eventEmitter, apiClientEmitter } = emitters;
const promise = request$.pipe(
map((res) => {
if (isHttpUploadProgressEvent(res)) {
const percent = Math.round((res.loaded / res.total) * 100);
eventEmitter.emit('progress', {loaded: res.loaded, total: res.total, percent});
}
const promise = request$
.pipe(
map((res) => {
if (isHttpUploadProgressEvent(res)) {
const percent = Math.round((res.loaded / res.total) * 100);
eventEmitter.emit('progress', { loaded: res.loaded, total: res.total, percent });
}
if (isHttpResponseEvent(res)) {
eventEmitter.emit('success', res.body);
return AdfHttpClient.deserialize(res, returnType);
}
}),
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
if (isHttpResponseEvent(res)) {
eventEmitter.emit('success', res.body);
return AdfHttpClient.deserialize(res, returnType);
}
}),
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
// we need to handle false positive cases here.
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
// we need to handle false positive cases here.
if (err.status === 200) {
eventEmitter.emit('success', err.error.text);
return of(err.error.text);
}
if (err.status === 200) {
eventEmitter.emit('success', err.error.text);
return of(err.error.text);
}
eventEmitter.emit('error', err);
apiClientEmitter.emit('error', { ...err, response: { req: err } });
eventEmitter.emit('error', err);
apiClientEmitter.emit('error', { ...err, response: { req: err } });
if (err.status === 401) {
eventEmitter.emit('unauthorized');
apiClientEmitter.emit('unauthorized');
}
if (err.status === 401) {
eventEmitter.emit('unauthorized');
apiClientEmitter.emit('unauthorized');
}
// for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it,
// and we need to be able to correctly pass instanceof Error conditions used inside repository
// we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error);
// for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it,
// and we need to be able to correctly pass instanceof Error conditions used inside repository
// we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error);
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
const error = {
...err,
body: err.error
};
const error = {
...err, body: err.error
};
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
return throwError(alfrescoApiError);
}),
takeUntil(abort$)
)
.toPromise();
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
return throwError(alfrescoApiError);
}),
takeUntil(abort$)
).toPromise();
(promise as any).abort = function() {
(promise as any).abort = function () {
eventEmitter.emit('abort');
abort$.next();
abort$.complete();
@@ -261,7 +249,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
}
if (isFormUrlEncoded) {
return new HttpParams({fromObject: removeNilValues(options.formParams)});
return new HttpParams({ fromObject: removeNilValues(options.formParams) });
}
return body;
@@ -273,8 +261,8 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
const optionsHeaders = {
...options.headerParams,
...(accept && {Accept: accept}),
...((contentType) && {'Content-Type': contentType})
...(accept && { Accept: accept }),
...(contentType && { 'Content-Type': contentType })
};
if (!this.disableCsrf) {
@@ -319,7 +307,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
return Boolean(contentType?.match(/^application\/json(;.*)?$/i));
}
private setCsrfToken(optionsHeaders: any) {
const token = this.createCSRFToken();
optionsHeaders['X-CSRF-TOKEN'] = token;
@@ -332,12 +319,16 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
}
private createCSRFToken(a?: any): string {
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
const randomValue = AdfHttpClient.getSecureRandomValue();
return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken);
}
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
private static getSecureRandomValue(): number {
const max = Math.pow(2, 32);
return window.crypto.getRandomValues(new Uint32Array(1))[0] / max;
}
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
if (isBlobType) {
@@ -359,7 +350,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
* @returns deserialized object
*/
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
if (response === null) {
return null;
}
@@ -390,9 +380,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
return new returnType(body);
}
private static deserializeBlobResponse(response: HttpResponse<Blob>) {
return new Blob([response.body], {type: response.headers.get('Content-Type')});
return new Blob([response.body], { type: response.headers.get('Content-Type') });
}
}