[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';
@@ -53,7 +45,6 @@ export interface Emitters {
providedIn: 'root'
})
export class AdfHttpClient implements ee.Emitter, JsApiHttpClient {
on: ee.EmitterMethod;
off: ee.EmitterMethod;
once: ee.EmitterMethod;
@@ -113,10 +104,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
emitters = this.getEventEmitters();
}
const request = this.httpClient.request(
options.httpMethod,
url,
{
const request = this.httpClient.request(options.httpMethod, url, {
context,
...(body && { body }),
...(responseType && { responseType }),
@@ -125,8 +113,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
headers,
observe: 'events',
reportProgress: true
}
);
});
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> {
const abort$ = new Subject<void>();
const { eventEmitter, apiClientEmitter } = emitters;
const promise = request$.pipe(
const promise = request$
.pipe(
map((res) => {
if (isHttpUploadProgressEvent(res)) {
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> => {
// 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.
@@ -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;
const error = {
...err, body: err.error
...err,
body: err.error
};
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
return throwError(alfrescoApiError);
}),
takeUntil(abort$)
).toPromise();
)
.toPromise();
(promise as any).abort = function () {
eventEmitter.emit('abort');
@@ -274,7 +262,7 @@ export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
const optionsHeaders = {
...options.headerParams,
...(accept && { Accept: accept }),
...((contentType) && {'Content-Type': contentType})
...(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') });
}
}

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 {
let ticket = null;
const contextRootBpm = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app';
const contextRoot = this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco';
const bpmRoot = `/${this.appConfig.get<string>(AppConfigValues.CONTEXTROOTBPM) || 'activiti-app'}/`;
const ecmRoot = `/${this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM) || 'alfresco'}/`;
if (contextRoot && requestUrl.indexOf(contextRoot) !== -1) {
ticket = 'Basic ' + btoa(this.contentAuth.getToken());
} else if (contextRootBpm && requestUrl.indexOf(contextRootBpm) !== -1) {
ticket = 'Basic ' + this.processAuth.getToken();
if (requestUrl?.includes(ecmRoot) && !requestUrl.includes(bpmRoot)) {
ticket = this.getContentServicesTicket();
} else if (requestUrl?.includes(bpmRoot) && !requestUrl.includes(ecmRoot)) {
ticket = this.getProcessServicesTicket();
} else if (requestUrl?.includes(ecmRoot) && requestUrl.includes(bpmRoot)) {
ticket = requestUrl.indexOf(ecmRoot) < requestUrl.indexOf(bpmRoot) ? this.getContentServicesTicket() : this.getProcessServicesTicket();
}
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());
}
}