[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' }); 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 { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api'; import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api';
import { import { HttpClient, HttpContext, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
HttpClient,
HttpContext,
HttpErrorResponse,
HttpEvent,
HttpHeaders,
HttpParams,
HttpResponse
} from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of, Subject, throwError } from 'rxjs'; import { Observable, of, Subject, throwError } from 'rxjs';
import { catchError, map, takeUntil } from 'rxjs/operators'; import { catchError, map, takeUntil } from 'rxjs/operators';
@@ -53,7 +45,6 @@ export interface Emitters {
providedIn: 'root' providedIn: 'root'
}) })
export class AdfHttpClient implements ee.Emitter, JsApiHttpClient { export class AdfHttpClient implements ee.Emitter, JsApiHttpClient {
on: ee.EmitterMethod; on: ee.EmitterMethod;
off: ee.EmitterMethod; off: ee.EmitterMethod;
once: ee.EmitterMethod; once: ee.EmitterMethod;
@@ -113,10 +104,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
emitters = this.getEventEmitters(); emitters = this.getEventEmitters();
} }
const request = this.httpClient.request( const request = this.httpClient.request(options.httpMethod, url, {
options.httpMethod,
url,
{
context, context,
...(body && { body }), ...(body && { body }),
...(responseType && { responseType }), ...(responseType && { responseType }),
@@ -125,8 +113,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
headers, headers,
observe: 'events', observe: 'events',
reportProgress: true reportProgress: true
} });
);
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType); return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
} }
@@ -189,11 +176,11 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
} }
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> { private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
const abort$ = new Subject<void>(); const abort$ = new Subject<void>();
const { eventEmitter, apiClientEmitter } = emitters; const { eventEmitter, apiClientEmitter } = emitters;
const promise = request$.pipe( const promise = request$
.pipe(
map((res) => { map((res) => {
if (isHttpUploadProgressEvent(res)) { if (isHttpUploadProgressEvent(res)) {
const percent = Math.round((res.loaded / res.total) * 100); const percent = Math.round((res.loaded / res.total) * 100);
@@ -206,7 +193,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
} }
}), }),
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => { 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 // 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. // we need to handle false positive cases here.
@@ -231,14 +217,16 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
// 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 = { const error = {
...err, body: err.error ...err,
body: err.error
}; };
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error); const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
return throwError(alfrescoApiError); return throwError(alfrescoApiError);
}), }),
takeUntil(abort$) takeUntil(abort$)
).toPromise(); )
.toPromise();
(promise as any).abort = function () { (promise as any).abort = function () {
eventEmitter.emit('abort'); eventEmitter.emit('abort');
@@ -274,7 +262,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
const optionsHeaders = { const optionsHeaders = {
...options.headerParams, ...options.headerParams,
...(accept && { Accept: accept }), ...(accept && { Accept: accept }),
...((contentType) && {'Content-Type': contentType}) ...(contentType && { 'Content-Type': contentType })
}; };
if (!this.disableCsrf) { if (!this.disableCsrf) {
@@ -319,7 +307,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
return Boolean(contentType?.match(/^application\/json(;.*)?$/i)); return Boolean(contentType?.match(/^application\/json(;.*)?$/i));
} }
private setCsrfToken(optionsHeaders: any) { private setCsrfToken(optionsHeaders: any) {
const token = this.createCSRFToken(); const token = this.createCSRFToken();
optionsHeaders['X-CSRF-TOKEN'] = token; optionsHeaders['X-CSRF-TOKEN'] = token;
@@ -332,12 +319,16 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
} }
private createCSRFToken(a?: any): string { 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); 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'; const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
if (isBlobType) { if (isBlobType) {
@@ -359,7 +350,6 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
* @returns deserialized object * @returns deserialized object
*/ */
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any { private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
if (response === null) { if (response === null) {
return null; return null;
} }
@@ -390,9 +380,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
return new returnType(body); return new returnType(body);
} }
private static deserializeBlobResponse(response: HttpResponse<Blob>) { 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') });
} }
} }

View File

@@ -0,0 +1,62 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { BasicAlfrescoAuthService } from './basic-alfresco-auth.service';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service';
import { ProcessAuth } from './process-auth';
import { ContentAuth } from './content-auth';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('BasicAlfrescoAuthService', () => {
let basicAlfrescoAuthService: BasicAlfrescoAuthService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [BasicAlfrescoAuthService]
});
basicAlfrescoAuthService = TestBed.inject(BasicAlfrescoAuthService);
spyOn(TestBed.inject(ProcessAuth), 'getToken').and.returnValue('Mock Process Auth ticket');
spyOn(TestBed.inject(ContentAuth), 'getToken').and.returnValue('Mock Content Auth ticket');
const appConfigSpy = spyOn(TestBed.inject(AppConfigService), 'get');
appConfigSpy.withArgs(AppConfigValues.CONTEXTROOTBPM).and.returnValue('activiti-app');
appConfigSpy.withArgs(AppConfigValues.CONTEXTROOTECM).and.returnValue('alfresco');
});
it('should return content services ticket when requestUrl contains ECM context root', () => {
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.exmple.com/alfresco/mock-api-url');
const base64Segment = ticket.split('Basic ')[1];
expect(atob(base64Segment)).toEqual('Mock Content Auth ticket');
});
it('should return process services ticket when requestUrl contains ECM context root', () => {
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.example.com/activiti-app/mock-api-url');
expect(ticket).toEqual('Basic Mock Process Auth ticket');
});
it('should return content services ticket when requestUrl contains both ECM and BPM context root, but ECM context root comes before', () => {
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.exmple.com/alfresco/activiti-app/mock-api-url');
const base64Segment = ticket.split('Basic ')[1];
expect(atob(base64Segment)).toEqual('Mock Content Auth ticket');
});
it('should return process services ticket when requestUrl contains both ECM and BPM context root, but BPM context root comes before', () => {
const ticket = basicAlfrescoAuthService.getTicketEcmBase64('http://www.example.com/activiti-app/alfresco/mock-api-url');
expect(ticket).toEqual('Basic Mock Process Auth ticket');
});
});

View File

@@ -373,15 +373,24 @@ export class BasicAlfrescoAuthService extends BaseAuthenticationService {
getTicketEcmBase64(requestUrl: string): string | null { getTicketEcmBase64(requestUrl: string): string | null {
let ticket = null; let ticket = null;
const contextRootBpm = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'; const bpmRoot = `/${this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'}/`;
const contextRoot = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco'; const ecmRoot = `/${this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco'}/`;
if (contextRoot && requestUrl.indexOf(contextRoot) !== -1) { if (requestUrl?.includes(ecmRoot) && !requestUrl.includes(bpmRoot)) {
ticket = 'Basic ' + btoa(this.contentAuth.getToken()); ticket = this.getContentServicesTicket();
} else if (contextRootBpm && requestUrl.indexOf(contextRootBpm) !== -1) { } else if (requestUrl?.includes(bpmRoot) && !requestUrl.includes(ecmRoot)) {
ticket = 'Basic ' + this.processAuth.getToken(); ticket = this.getProcessServicesTicket();
} else if (requestUrl?.includes(ecmRoot) && requestUrl.includes(bpmRoot)) {
ticket = requestUrl.indexOf(ecmRoot) < requestUrl.indexOf(bpmRoot) ? this.getContentServicesTicket() : this.getProcessServicesTicket();
} }
return ticket; return ticket;
} }
private getProcessServicesTicket(): string {
return this.processAuth.getToken()?.startsWith('Basic ') ? this.processAuth.getToken() : 'Basic ' + this.processAuth.getToken();
}
private getContentServicesTicket(): string {
return 'Basic ' + btoa(this.contentAuth.getToken());
}
} }