mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-39851 replace coreAuth with provideBffAuth to call BFF
This commit is contained in:
@@ -20,6 +20,7 @@ import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } fr
|
||||
import { AuthenticationService } from '../services/authentication.service';
|
||||
import { AuthGuardService } from './auth-guard.service';
|
||||
import { JwtHelperService } from '../services/jwt-helper.service';
|
||||
import { BffAuthGuard } from '../services/bff/bff-auth.guard';
|
||||
|
||||
const ticketChangeRedirect = (event: StorageEvent, authGuardBaseService: AuthGuardService, url: string): void => {
|
||||
if (event.newValue) {
|
||||
@@ -45,7 +46,7 @@ const ticketChangeHandler = (event: StorageEvent, authGuardBaseService: AuthGuar
|
||||
}
|
||||
};
|
||||
|
||||
export const AuthGuard: CanActivateFn = async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> => {
|
||||
export const LegacyAuthGuard: CanActivateFn = async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> => {
|
||||
const router = inject(Router);
|
||||
const jwtHelperService = inject(JwtHelperService);
|
||||
const authGuardBaseService = inject(AuthGuardService);
|
||||
@@ -59,3 +60,4 @@ export const AuthGuard: CanActivateFn = async (_: ActivatedRouteSnapshot, state:
|
||||
|
||||
return authGuardBaseService.redirectToUrl(state.url);
|
||||
};
|
||||
export const AuthGuard: CanActivateFn = BffAuthGuard;
|
||||
|
||||
@@ -16,18 +16,22 @@
|
||||
*/
|
||||
|
||||
import { inject, ModuleWithProviders, NgModule, InjectionToken, provideAppInitializer, EnvironmentProviders, Provider } from '@angular/core';
|
||||
import { AUTH_CONFIG, OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc';
|
||||
import { AUTH_CONFIG, OAuthService, OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc';
|
||||
import { AuthenticationService } from '../services/authentication.service';
|
||||
import { AuthModuleConfig, AUTH_MODULE_CONFIG } from './auth-config';
|
||||
import { authConfigFactory, AuthConfigService } from './auth-config.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import { RedirectAuthService } from './redirect-auth.service';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi, withXsrfConfiguration } from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptors, withInterceptorsFromDi, withXsrfConfiguration } from '@angular/common/http';
|
||||
import { TokenInterceptor } from './token.interceptor';
|
||||
import { StorageService } from '../../common/services/storage.service';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { AUTH_ROUTES } from './auth.routes';
|
||||
import { Authentication, AuthenticationInterceptor } from '@alfresco/adf-core/auth';
|
||||
import { BffAuthService } from '../services/bff/bff-auth.service';
|
||||
import { UserAccessService } from '../services/user-access.service';
|
||||
import { BffUserAccessService } from '../services/bff/bff-user-access.service';
|
||||
import { bffAuthErrorInterceptor } from '../services/bff/bff-auth-error.interceptor';
|
||||
|
||||
export const JWT_STORAGE_SERVICE = new InjectionToken<OAuthStorage>('JWT_STORAGE_SERVICE', {
|
||||
providedIn: 'root',
|
||||
@@ -73,6 +77,31 @@ export function provideCoreAuth(config: AuthModuleConfig = { useHash: false }):
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the necessary Angular providers for BFF (Backend For Frontend) authentication.
|
||||
*
|
||||
* This function returns an array of providers required to set up authentication using the BffAuthService.
|
||||
* It includes HTTP client, router configuration with BFF-specific routes, and maps authentication services
|
||||
* to the BffAuthService implementation.
|
||||
*
|
||||
* @returns An array of Angular providers for BFF authentication.
|
||||
*/
|
||||
export function provideBffAuth(): (Provider | EnvironmentProviders)[] {
|
||||
return [
|
||||
provideHttpClient(
|
||||
withXsrfConfiguration({ cookieName: 'CSRF-TOKEN', headerName: 'X-CSRF-TOKEN' }),
|
||||
withInterceptors([bffAuthErrorInterceptor])
|
||||
),
|
||||
BffAuthService,
|
||||
{ provide: UserAccessService, useClass: BffUserAccessService },
|
||||
{ provide: OAuthStorage, useFactory: () => ({ getItem: () => null, setItem: () => null, removeItem: () => null }) },
|
||||
{ provide: OAuthService, useFactory: () => ({}) },
|
||||
{ provide: AUTH_MODULE_CONFIG, useFactory: () => ({ useHash: false, preventClearHashAfterLogin: true }) },
|
||||
{ provide: AuthService, useExisting: BffAuthService },
|
||||
{ provide: AuthenticationService, useExisting: BffAuthService }
|
||||
];
|
||||
}
|
||||
|
||||
/** @deprecated use `provideCoreAuth()` provider api instead */
|
||||
@NgModule({
|
||||
providers: [...provideCoreAuth()]
|
||||
|
||||
@@ -31,6 +31,9 @@ export * from './services/jwt-helper.service';
|
||||
export * from './services/oauth2.service';
|
||||
export * from './services/user-access.service';
|
||||
|
||||
export * from './services/bff/bff-auth.service';
|
||||
export * from './services/bff/bff-auth.guard';
|
||||
|
||||
export * from './basic-auth/basic-alfresco-auth.service';
|
||||
export * from './basic-auth/process-auth';
|
||||
export * from './basic-auth/content-auth';
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { HttpErrorResponse, HttpRequest } from '@angular/common/http';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { EMPTY, throwError, firstValueFrom } from 'rxjs';
|
||||
import { bffAuthErrorInterceptor } from './bff-auth-error.interceptor';
|
||||
|
||||
describe('bffAuthErrorInterceptor', () => {
|
||||
it('should return EMPTY when 401 error occurs on /bff/ URL', async () => {
|
||||
const req = new HttpRequest('GET', '/bff/resource');
|
||||
const httpError = new HttpErrorResponse({ status: 401, url: '/bff/resource' });
|
||||
const next = () => throwError(() => httpError);
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => bffAuthErrorInterceptor(req, next));
|
||||
|
||||
const result = await firstValueFrom(result$, { defaultValue: null });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should rethrow error when 401 on non-/bff/ URL', async () => {
|
||||
const req = new HttpRequest('GET', '/api/resource');
|
||||
const httpError = new HttpErrorResponse({ status: 401, url: '/api/resource' });
|
||||
const next = () => throwError(() => httpError);
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => bffAuthErrorInterceptor(req, next));
|
||||
|
||||
await expectAsync(firstValueFrom(result$)).toBeRejectedWith(httpError);
|
||||
});
|
||||
|
||||
it('should rethrow error when status is not 401 even on /bff/ URL', async () => {
|
||||
const req = new HttpRequest('GET', '/bff/resource');
|
||||
const httpError = new HttpErrorResponse({ status: 500, url: '/bff/resource' });
|
||||
const next = () => throwError(() => httpError);
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => bffAuthErrorInterceptor(req, next));
|
||||
|
||||
await expectAsync(firstValueFrom(result$)).toBeRejectedWith(httpError);
|
||||
});
|
||||
|
||||
it('should pass through successful response without intercepting', async () => {
|
||||
const req = new HttpRequest('GET', '/bff/resource');
|
||||
const next = () => EMPTY;
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => bffAuthErrorInterceptor(req, next));
|
||||
|
||||
const result = await firstValueFrom(result$, { defaultValue: null });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { EMPTY, throwError } from 'rxjs';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
export const bffAuthErrorInterceptor: HttpInterceptorFn = (req, next) =>
|
||||
next(req).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
console.log('%c[bffAuthErrorInterceptor] err: ', 'color: red;', err);
|
||||
if (err.status === 401 && req.url.includes('/bff/')) {
|
||||
const returnUrl = window.location.pathname + window.location.search;
|
||||
window.location.href = `/bff/login?returnUrl=${encodeURIComponent(returnUrl)}`;
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,125 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { firstValueFrom, Observable, of, throwError } from 'rxjs';
|
||||
import { BffAuthGuard } from './bff-auth.guard';
|
||||
import { BffAuthService, BffUserResponse } from './bff-auth.service';
|
||||
|
||||
describe('BffAuthGuard', () => {
|
||||
let bffAuthService: jasmine.SpyObj<BffAuthService>;
|
||||
let mockRoute: ActivatedRouteSnapshot;
|
||||
let mockState: RouterStateSnapshot;
|
||||
|
||||
const mockAuthenticatedUser: BffUserResponse = {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
sub: 'user123',
|
||||
email: 'user@example.com',
|
||||
hxp_account: 'account123',
|
||||
name: 'Test User',
|
||||
email_verified: true,
|
||||
preferred_username: 'testuser',
|
||||
given_name: 'Test',
|
||||
family_name: 'User',
|
||||
roles: ['ROLE_USER'],
|
||||
appKey: 'app123'
|
||||
}
|
||||
};
|
||||
|
||||
const mockUnauthenticatedUser: BffUserResponse = {
|
||||
isAuthenticated: false,
|
||||
user: {
|
||||
sub: '',
|
||||
email: '',
|
||||
hxp_account: '',
|
||||
name: '',
|
||||
email_verified: false,
|
||||
preferred_username: '',
|
||||
given_name: '',
|
||||
family_name: '',
|
||||
roles: [],
|
||||
appKey: ''
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
bffAuthService = jasmine.createSpyObj('BffAuthService', ['getUser', 'login']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: BffAuthService, useValue: bffAuthService }]
|
||||
});
|
||||
|
||||
mockRoute = {} as ActivatedRouteSnapshot;
|
||||
mockState = { url: '/protected-route' } as RouterStateSnapshot;
|
||||
});
|
||||
|
||||
it('should allow navigation when user is authenticated', async () => {
|
||||
bffAuthService.getUser.and.returnValue(of(mockAuthenticatedUser));
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => BffAuthGuard(mockRoute, mockState)) as Observable<boolean>;
|
||||
const result = await firstValueFrom(result$);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(bffAuthService.getUser).toHaveBeenCalled();
|
||||
expect(bffAuthService.login).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny navigation and call login when user is not authenticated', async () => {
|
||||
bffAuthService.getUser.and.returnValue(of(mockUnauthenticatedUser));
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => BffAuthGuard(mockRoute, mockState)) as Observable<boolean>;
|
||||
const result = await firstValueFrom(result$);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(bffAuthService.getUser).toHaveBeenCalled();
|
||||
expect(bffAuthService.login).toHaveBeenCalledWith('/protected-route');
|
||||
});
|
||||
|
||||
it('should deny navigation and call login when getUser throws an error', async () => {
|
||||
const error = new Error('Network error');
|
||||
bffAuthService.getUser.and.returnValue(throwError(() => error));
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => BffAuthGuard(mockRoute, mockState)) as Observable<boolean>;
|
||||
const result = await firstValueFrom(result$);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(bffAuthService.getUser).toHaveBeenCalled();
|
||||
expect(bffAuthService.login).toHaveBeenCalledWith('/protected-route');
|
||||
});
|
||||
|
||||
it('should pass correct state URL to login method', async () => {
|
||||
const customState = { url: '/custom/path?query=123' } as RouterStateSnapshot;
|
||||
bffAuthService.getUser.and.returnValue(of(mockUnauthenticatedUser));
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => BffAuthGuard(mockRoute, customState)) as Observable<boolean>;
|
||||
await firstValueFrom(result$);
|
||||
|
||||
expect(bffAuthService.login).toHaveBeenCalledWith('/custom/path?query=123');
|
||||
});
|
||||
|
||||
it('should handle empty state URL', async () => {
|
||||
const emptyState = { url: '' } as RouterStateSnapshot;
|
||||
bffAuthService.getUser.and.returnValue(of(mockUnauthenticatedUser));
|
||||
|
||||
const result$ = TestBed.runInInjectionContext(() => BffAuthGuard(mockRoute, emptyState)) as Observable<boolean>;
|
||||
await firstValueFrom(result$);
|
||||
|
||||
expect(bffAuthService.login).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { CanActivateFn } from '@angular/router';
|
||||
import { catchError, map, of } from 'rxjs';
|
||||
import { BffAuthService, BffUserResponse } from './bff-auth.service';
|
||||
import { inject } from '@angular/core';
|
||||
/* eslint-disable no-console */
|
||||
/**
|
||||
* Guard function for route activation that checks user authentication via BffAuthService.
|
||||
* If the user is unauthenticated, it handles redirection and triggers login.
|
||||
*
|
||||
* @param _route - The activated route snapshot (unused).
|
||||
* @param state - The router state snapshot containing the target URL.
|
||||
* @returns An Observable emitting `true` if the user is authenticated, or `false` otherwise.
|
||||
*/
|
||||
export const BffAuthGuard: CanActivateFn = (_route, state) => {
|
||||
const auth = inject(BffAuthService);
|
||||
|
||||
return auth.getUser().pipe(
|
||||
map((userResponse) => handleAuthRedirectIfUnauthenticated(userResponse, state.url, auth)),
|
||||
catchError((error) => {
|
||||
console.error('[BffAuthGuard] error: ', error);
|
||||
console.error('[BffAuthGuard] state.url: ', state.url);
|
||||
auth.login(state.url);
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles authentication redirect if the user is unauthenticated.
|
||||
*
|
||||
* @param userResponse The response object containing authentication status.
|
||||
* @param url The URL to redirect to after authentication.
|
||||
* @param auth The BffAuthService instance used for authentication actions.
|
||||
* @returns True if the user is authenticated, otherwise false.
|
||||
*/
|
||||
function handleAuthRedirectIfUnauthenticated(userResponse: BffUserResponse, url: string, auth: BffAuthService): boolean {
|
||||
console.log('%c[BffAuthGuard] userResponse.isAuthenticated: ', 'color: orange;', userResponse.isAuthenticated);
|
||||
if (userResponse.isAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
console.log('%c[BffAuthGuard] not authenticated, redirect to bff/login, state.url: ', 'color: orange;', url);
|
||||
auth.login(url);
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { BffAuthService, BffUserResponse } from './bff-auth.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { provideHttpClient, HttpHeaders } from '@angular/common/http';
|
||||
|
||||
describe('BffAuthService', () => {
|
||||
let service: BffAuthService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
const mockAuthenticatedUser: BffUserResponse = {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
sub: 'user-123',
|
||||
email: 'test@example.com',
|
||||
hxp_account: 'account-456',
|
||||
name: 'Test User',
|
||||
email_verified: true,
|
||||
preferred_username: 'testuser',
|
||||
given_name: 'Test',
|
||||
family_name: 'User',
|
||||
roles: ['admin', 'user'],
|
||||
appKey: 'app-key-789'
|
||||
}
|
||||
};
|
||||
|
||||
const mockUnauthenticatedUser: BffUserResponse = {
|
||||
isAuthenticated: false,
|
||||
user: {
|
||||
sub: '',
|
||||
email: '',
|
||||
hxp_account: '',
|
||||
name: '',
|
||||
email_verified: false,
|
||||
preferred_username: '',
|
||||
given_name: '',
|
||||
family_name: '',
|
||||
roles: [],
|
||||
appKey: ''
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BffAuthService, provideHttpClient(), provideHttpClientTesting()]
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('getUser', () => {
|
||||
it('should return authenticated user when user is logged in', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
const resultPromise = firstValueFrom(service.getUser());
|
||||
|
||||
const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user'));
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockAuthenticatedUser);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toEqual(mockAuthenticatedUser);
|
||||
expect(result.isAuthenticated).toBe(true);
|
||||
expect(result.user.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should return unauthenticated user when user is not logged in', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser));
|
||||
|
||||
const resultPromise = firstValueFrom(service.getUser());
|
||||
|
||||
const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user'));
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockUnauthenticatedUser);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toEqual(mockUnauthenticatedUser);
|
||||
expect(result.isAuthenticated).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle error when getUser fails', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }));
|
||||
|
||||
try {
|
||||
const result = firstValueFrom(service.getUser());
|
||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/user'));
|
||||
req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
|
||||
await result;
|
||||
fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error.status).toBe(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('should use correct URL format with protocol and host', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.getUser().subscribe();
|
||||
|
||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/user'));
|
||||
expect(req.request.url).toMatch(/^https?:\/\/.+\/bff\/user$/);
|
||||
req.flush(mockAuthenticatedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should redirect to /bff/login when no returnUrl is provided', () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.login();
|
||||
});
|
||||
|
||||
it('should redirect to /bff/login when returnUrl is root', () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.login('/');
|
||||
});
|
||||
|
||||
it('should redirect to /bff/login with returnUrl when currentUrl is provided', () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.login('/dashboard');
|
||||
});
|
||||
|
||||
it('should properly encode returnUrl parameter', () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.login('/path?query=value&other=data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call /bff/logout and redirect to default location on success', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.logout();
|
||||
|
||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({});
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('should redirect to custom redirectTo location when provided', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.logout();
|
||||
|
||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
|
||||
req.flush({ redirectTo: '/custom-logout' });
|
||||
});
|
||||
|
||||
it('should reload page on logout error', async () => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.logout();
|
||||
|
||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
|
||||
req.flush('Server Error', { status: 500, statusText: 'Server Error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should set isAuthenticated to true when user is authenticated', (done) => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
service.onLogin.subscribe((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
expect(service.isAuthenticated).toBe(true);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit onLogin when user is not authenticated', (done) => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser));
|
||||
|
||||
let loginEmitted = false;
|
||||
service.onLogin.subscribe((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
loginEmitted = true;
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(loginEmitted).toBe(false);
|
||||
expect(service.isAuthenticated).toBe(false);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should populate userInfo from getUser response', (done) => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
|
||||
setTimeout(() => {
|
||||
expect(service.userInfo).toEqual(mockAuthenticatedUser);
|
||||
expect(service.userInfo.user.email).toBe('test@example.com');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interface methods', () => {
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(BffAuthService);
|
||||
|
||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
||||
expect(bffUserRequestMatchers.length).toBe(2);
|
||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||
});
|
||||
|
||||
it('should return empty string for getToken', () => {
|
||||
expect(service.getToken()).toBe('');
|
||||
});
|
||||
|
||||
it('should return isAuthenticated for isLoggedIn', () => {
|
||||
service.isAuthenticated = true;
|
||||
expect(service.isLoggedIn()).toBe(true);
|
||||
|
||||
service.isAuthenticated = false;
|
||||
expect(service.isLoggedIn()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for isOauth', () => {
|
||||
expect(service.isOauth()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for isECMProvider', () => {
|
||||
expect(service.isECMProvider()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for isBPMProvider', () => {
|
||||
expect(service.isBPMProvider()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for isALLProvider', () => {
|
||||
expect(service.isALLProvider()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return empty string for getUsername', () => {
|
||||
expect(service.getUsername()).toBe('');
|
||||
});
|
||||
|
||||
it('should return header unchanged for getAuthHeaders', () => {
|
||||
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
const result = service.getAuthHeaders('/test', headers);
|
||||
expect(result).toBe(headers);
|
||||
});
|
||||
|
||||
it('should return headers as observable for addTokenToHeader', async () => {
|
||||
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
const result = await firstValueFrom(service.addTokenToHeader('/test', headers));
|
||||
expect(result).toBe(headers);
|
||||
});
|
||||
|
||||
it('should handle addTokenToHeader with no headers argument', async () => {
|
||||
const result = await firstValueFrom(service.addTokenToHeader('/test'));
|
||||
expect(result).toBeInstanceOf(HttpHeaders);
|
||||
});
|
||||
|
||||
it('should have reset method that does nothing', () => {
|
||||
expect(() => service.reset()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, filter, Observable } from 'rxjs';
|
||||
import { AuthenticationServiceInterface } from '../../interfaces/authentication-service.interface';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
export interface BffUserInfo {
|
||||
sub: string;
|
||||
email: string;
|
||||
hxp_account: string;
|
||||
name: string;
|
||||
email_verified: boolean;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
roles: string[];
|
||||
appKey: string;
|
||||
}
|
||||
|
||||
export interface BffUserResponse {
|
||||
isAuthenticated: boolean;
|
||||
user: BffUserInfo;
|
||||
}
|
||||
type EventEmitterInstance = InstanceType<typeof EventEmitter>;
|
||||
|
||||
/* eslint-disable no-console */
|
||||
@Injectable()
|
||||
export class BffAuthService implements AuthenticationServiceInterface {
|
||||
isAuthenticated: boolean = false;
|
||||
username: string = '';
|
||||
|
||||
userInfo: BffUserResponse;
|
||||
|
||||
onError = new BehaviorSubject<any>(null);
|
||||
onLogin = new BehaviorSubject<boolean>(false);
|
||||
onLogout = new BehaviorSubject<boolean>(false);
|
||||
|
||||
on: EventEmitterInstance['on'];
|
||||
off: EventEmitterInstance['off'];
|
||||
once: EventEmitterInstance['once'];
|
||||
emit: EventEmitterInstance['emit'];
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.getUser()
|
||||
.pipe(filter((user) => user.isAuthenticated))
|
||||
.subscribe(() => {
|
||||
this.isAuthenticated = true;
|
||||
this.onLogin.next(this.isAuthenticated);
|
||||
});
|
||||
this.getUser().subscribe((userResponse) => {
|
||||
console.log('[BffAuthService] userResponse: ', userResponse);
|
||||
this.userInfo = userResponse;
|
||||
});
|
||||
}
|
||||
|
||||
getToken(): string {
|
||||
return '';
|
||||
}
|
||||
isLoggedIn(): boolean {
|
||||
return this.isAuthenticated;
|
||||
}
|
||||
isOauth(): boolean {
|
||||
return true;
|
||||
}
|
||||
isECMProvider(): boolean {
|
||||
return false;
|
||||
}
|
||||
isBPMProvider(): boolean {
|
||||
return false;
|
||||
}
|
||||
isALLProvider(): boolean {
|
||||
return false;
|
||||
}
|
||||
getUsername(): string {
|
||||
return '';
|
||||
}
|
||||
getAuthHeaders(_requestUrl: string, header: HttpHeaders): HttpHeaders {
|
||||
return header;
|
||||
}
|
||||
addTokenToHeader(_requestUrl: string, headersArg?: HttpHeaders): Observable<HttpHeaders> {
|
||||
return new BehaviorSubject(headersArg ?? new HttpHeaders()).asObservable();
|
||||
}
|
||||
reset(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
getUser(): Observable<BffUserResponse> {
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
console.log('[BffAuthService] getUser from ', `${protocol}//${host}/bff/user`);
|
||||
return this.http.get<BffUserResponse>(`${protocol}//${host}/bff/user`);
|
||||
}
|
||||
|
||||
login(currentUrl?: string): Promise<void> | void {
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
let url: string;
|
||||
if (!currentUrl || currentUrl === '/') {
|
||||
url = `${protocol}//${host}/bff/login`;
|
||||
} else {
|
||||
url = `${protocol}//${host}/bff/login?returnUrl=${encodeURIComponent(currentUrl ?? '')}`;
|
||||
}
|
||||
|
||||
console.log('url: ', url);
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
logout(): Promise<void> | void {
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
this.http.post<{ redirectTo?: string }>(`${protocol}//${host}/bff/logout`, {}).subscribe({
|
||||
next: (res) => {
|
||||
console.log('[BffAuthService] logout: ', res);
|
||||
const target = res.redirectTo || '/';
|
||||
window.location.href = target;
|
||||
},
|
||||
error: () => window.location.reload()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { BffAuthService, BffUserInfo } from './bff-auth.service';
|
||||
import { BffUserAccessService } from './bff-user-access.service';
|
||||
|
||||
describe('BffUserAccessService', () => {
|
||||
let service: BffUserAccessService;
|
||||
let bffAuthServiceSpy: jasmine.SpyObj<BffAuthService>;
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
function createMockUserInfo(appKey: string, roles: string[]): BffUserInfo {
|
||||
return {
|
||||
appKey,
|
||||
roles,
|
||||
sub: '',
|
||||
email: '',
|
||||
hxp_account: '',
|
||||
name: '',
|
||||
email_verified: false,
|
||||
preferred_username: '',
|
||||
given_name: '',
|
||||
family_name: ''
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const spy = jasmine.createSpyObj('BffAuthService', ['getUser', 'login', 'logout']);
|
||||
spy.userInfo = {
|
||||
authenticated: true,
|
||||
user: createMockUserInfo('test-app', ['ROLE_USER', 'ROLE_ADMIN'])
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BffUserAccessService, { provide: BffAuthService, useValue: spy }]
|
||||
});
|
||||
|
||||
service = TestBed.inject(BffUserAccessService);
|
||||
bffAuthServiceSpy = TestBed.inject(BffAuthService) as jasmine.SpyObj<BffAuthService>;
|
||||
});
|
||||
|
||||
it('should create the service', () => {
|
||||
expect(service).toBeInstanceOf(BffUserAccessService);
|
||||
});
|
||||
|
||||
describe('fetchUserAccess', () => {
|
||||
it('should set applicationAccess when user has valid appKey and roles', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER', 'ROLE_ADMIN']);
|
||||
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(true);
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should set applicationAccess to null when appKey is empty', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('', ['ROLE_USER']);
|
||||
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should set applicationAccess to null when roles array is empty', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', []);
|
||||
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should set applicationAccess to null when roles is undefined', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', undefined as unknown as string[]);
|
||||
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple calls to fetchUserAccess', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('app1', ['ROLE1']);
|
||||
service.fetchUserAccess();
|
||||
expect(service.hasApplicationAccess('app1', ['ROLE1'])).toBe(true);
|
||||
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('app2', ['ROLE2']);
|
||||
service.fetchUserAccess();
|
||||
expect(service.hasApplicationAccess('app2', ['ROLE2'])).toBe(true);
|
||||
expect(service.hasApplicationAccess('app1', ['ROLE1'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasGlobalAccess', () => {
|
||||
describe('when rolesToCheck is empty or not provided', () => {
|
||||
it('should return true when rolesToCheck is empty array', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess([])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when rolesToCheck is null', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(null as unknown as string[])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when rolesToCheck is undefined', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(undefined as unknown as string[])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user has required roles', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER', 'ROLE_ADMIN', 'ROLE_VIEWER']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return true when user has one of the required roles', () => {
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when user has all of the required roles', () => {
|
||||
expect(service.hasGlobalAccess(['ROLE_USER', 'ROLE_ADMIN'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when user has at least one of multiple required roles', () => {
|
||||
expect(service.hasGlobalAccess(['ROLE_NONEXISTENT', 'ROLE_ADMIN'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should be case-sensitive when checking roles', () => {
|
||||
expect(service.hasGlobalAccess(['role_user'])).toBe(false);
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user lacks required roles', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return false when user has none of the required roles', () => {
|
||||
expect(service.hasGlobalAccess(['ROLE_ADMIN', 'ROLE_SUPERUSER'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applicationAccess is not set', () => {
|
||||
it('should return false when applicationAccess is null (empty appKey)', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when applicationAccess is null (empty roles)', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', []);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when fetchUserAccess has not been called', () => {
|
||||
expect(service.hasGlobalAccess(['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasApplicationAccess', () => {
|
||||
describe('when appName is invalid', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return false when appName is empty string', () => {
|
||||
expect(service.hasApplicationAccess('', ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when appName is null', () => {
|
||||
expect(service.hasApplicationAccess(null as unknown as string, ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when appName is undefined', () => {
|
||||
expect(service.hasApplicationAccess(undefined as unknown as string, ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rolesToCheck is empty or not provided', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return true when rolesToCheck is empty array', () => {
|
||||
expect(service.hasApplicationAccess('my-app', [])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when rolesToCheck is null', () => {
|
||||
expect(service.hasApplicationAccess('my-app', null as unknown as string[])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when rolesToCheck is undefined', () => {
|
||||
expect(service.hasApplicationAccess('my-app', undefined as unknown as string[])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when app name matches and user has roles', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER', 'ROLE_ADMIN']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return true when user has the required role', () => {
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when user has at least one of the required roles', () => {
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_NONEXISTENT', 'ROLE_ADMIN'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should be case-sensitive when checking app name', () => {
|
||||
expect(service.hasApplicationAccess('MY-APP', ['ROLE_USER'])).toBe(false);
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should be case-sensitive when checking roles', () => {
|
||||
expect(service.hasApplicationAccess('my-app', ['role_user'])).toBe(false);
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when app name does not match', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return false even if user has the required roles', () => {
|
||||
expect(service.hasApplicationAccess('different-app', ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user lacks required roles', () => {
|
||||
beforeEach(() => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
});
|
||||
|
||||
it('should return false when user has none of the required roles', () => {
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_ADMIN', 'ROLE_SUPERUSER'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applicationAccess is not set', () => {
|
||||
it('should return false when applicationAccess is null (empty appKey)', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('', ['ROLE_USER']);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when applicationAccess is null (empty roles)', () => {
|
||||
bffAuthServiceSpy.userInfo.user = createMockUserInfo('my-app', []);
|
||||
service.fetchUserAccess();
|
||||
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when fetchUserAccess has not been called', () => {
|
||||
expect(service.hasApplicationAccess('my-app', ['ROLE_USER'])).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 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 { inject, Injectable } from '@angular/core';
|
||||
import { IUserAccessService } from '../user-access.service';
|
||||
import { ApplicationAccessModel } from '../../models/application-access.model';
|
||||
import { BffAuthService } from './bff-auth.service';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
@Injectable()
|
||||
export class BffUserAccessService implements IUserAccessService {
|
||||
private applicationAccess: ApplicationAccessModel;
|
||||
private bffAuthService = inject(BffAuthService);
|
||||
|
||||
fetchUserAccess() {
|
||||
console.log('[BffUserAccessService] fetchUserAccess');
|
||||
const { user } = this.bffAuthService.userInfo;
|
||||
if (user.appKey && user.roles?.length > 0) {
|
||||
this.applicationAccess = {
|
||||
name: user.appKey,
|
||||
roles: user.roles
|
||||
};
|
||||
console.log('✅[BffUserAccessService] fetchUserAccess applicationAccess: ', this.applicationAccess);
|
||||
} else {
|
||||
this.applicationAccess = null;
|
||||
}
|
||||
}
|
||||
|
||||
hasGlobalAccess(rolesToCheck: string[]): boolean {
|
||||
if (!rolesToCheck?.length) {
|
||||
return true;
|
||||
}
|
||||
const hasAccess = this.applicationAccess?.roles?.some((role) => rolesToCheck.includes(role)) ?? false;
|
||||
console.log('[BffUserAccessService] hasGlobalAccess() hasAccess: ', hasAccess);
|
||||
return hasAccess;
|
||||
}
|
||||
|
||||
hasApplicationAccess(appName: string, rolesToCheck: string[]): boolean {
|
||||
if (!appName) {
|
||||
return false;
|
||||
}
|
||||
if (!rolesToCheck?.length) {
|
||||
return true;
|
||||
}
|
||||
const { name, roles } = this.applicationAccess || {};
|
||||
if (name !== appName || !roles?.length) {
|
||||
return false;
|
||||
}
|
||||
return roles.some((role) => rolesToCheck.includes(role));
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,23 @@ import { JwtHelperService } from './jwt-helper.service';
|
||||
import { ApplicationAccessModel } from '../models/application-access.model';
|
||||
import { AppConfigService } from '../../app-config/app-config.service';
|
||||
|
||||
export interface IUserAccessService {
|
||||
fetchUserAccess(): void;
|
||||
hasGlobalAccess(rolesToCheck: string[]): boolean;
|
||||
hasApplicationAccess(appName: string, rolesToCheck: string[]): boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserAccessService {
|
||||
export class UserAccessService implements IUserAccessService {
|
||||
private globalAccess: string[];
|
||||
private applicationAccess: ApplicationAccessModel[];
|
||||
|
||||
constructor(private jwtHelperService: JwtHelperService, private appConfigService: AppConfigService) {}
|
||||
constructor(
|
||||
private jwtHelperService: JwtHelperService,
|
||||
private appConfigService: AppConfigService
|
||||
) {}
|
||||
|
||||
fetchUserAccess() {
|
||||
if (this.hasRolesInRealmAccess()) {
|
||||
|
||||
Reference in New Issue
Block a user