[AAE-15018] - Add support for roles as part of the access token authorization (#8642)

This commit is contained in:
Ardit Domi 2023-06-27 09:57:40 +01:00 committed by GitHub
parent b16777cce0
commit f0d2456f42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 146 deletions

View File

@ -23,14 +23,12 @@ import { AuthGuardSsoRoleService } from './auth-guard-sso-role.service';
import { JwtHelperService } from '../services/jwt-helper.service'; import { JwtHelperService } from '../services/jwt-helper.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { UserAccessService } from '../services/user-access.service';
describe('Auth Guard SSO role service', () => { describe('Auth Guard SSO role service', () => {
let authGuard: AuthGuardSsoRoleService; let authGuard: AuthGuardSsoRoleService;
let jwtHelperService: JwtHelperService; let jwtHelperService: JwtHelperService;
let routerService: Router; let routerService: Router;
let userAccessService: UserAccessService;
setupTestBed({ setupTestBed({
imports: [ imports: [
@ -44,8 +42,6 @@ describe('Auth Guard SSO role service', () => {
authGuard = TestBed.inject(AuthGuardSsoRoleService); authGuard = TestBed.inject(AuthGuardSsoRoleService);
jwtHelperService = TestBed.inject(JwtHelperService); jwtHelperService = TestBed.inject(JwtHelperService);
routerService = TestBed.inject(Router); routerService = TestBed.inject(Router);
userAccessService = TestBed.inject(UserAccessService);
userAccessService.resetAccess();
}); });
function spyUserAccess(realmRoles: string[], resourceAccess: any) { function spyUserAccess(realmRoles: string[], resourceAccess: any) {

View File

@ -29,8 +29,8 @@ export class AuthGuardSsoRoleService implements CanActivate {
private dialog: MatDialog) { private dialog: MatDialog) {
} }
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { canActivate(route: ActivatedRouteSnapshot): boolean {
await this.userAccessService.fetchUserAccess(); this.userAccessService.fetchUserAccess();
let hasRealmRole = false; let hasRealmRole = false;
let hasClientRole = true; let hasClientRole = true;
if (route.data) { if (route.data) {
@ -40,7 +40,7 @@ export class AuthGuardSsoRoleService implements CanActivate {
hasRealmRole = true; hasRealmRole = true;
} else { } else {
const excludedRoles = route.data['excludedRoles'] || []; const excludedRoles = route.data['excludedRoles'] || [];
hasRealmRole = await this.validateRoles(rolesToCheck, excludedRoles); hasRealmRole = this.validateRoles(rolesToCheck, excludedRoles);
} }
} }
@ -63,14 +63,14 @@ export class AuthGuardSsoRoleService implements CanActivate {
return hasRole; return hasRole;
} }
private async validateRoles(rolesToCheck: string[], excludedRoles?: string[]): Promise<boolean> { private validateRoles(rolesToCheck: string[], excludedRoles?: string[]): boolean {
if (excludedRoles?.length > 0) { if (excludedRoles?.length > 0) {
return await this.hasRoles(rolesToCheck) && !await this.hasRoles(excludedRoles); return this.hasRoles(rolesToCheck) && !this.hasRoles(excludedRoles);
} }
return this.hasRoles(rolesToCheck); return this.hasRoles(rolesToCheck);
} }
private async hasRoles(roles: string[] = []): Promise<boolean> { private hasRoles(roles: string[] = []): boolean {
return this.userAccessService.hasGlobalAccess(roles); return this.userAccessService.hasGlobalAccess(roles);
} }

View File

@ -32,6 +32,7 @@ export class JwtHelperService {
static REALM_ACCESS = 'realm_access'; static REALM_ACCESS = 'realm_access';
static RESOURCE_ACCESS = 'resource_access'; static RESOURCE_ACCESS = 'resource_access';
static USER_PREFERRED_USERNAME = 'preferred_username'; static USER_PREFERRED_USERNAME = 'preferred_username';
static HXP_AUTHORIZATION = 'hxp_authorization';
constructor(private storageService: StorageService) { constructor(private storageService: StorageService) {
} }

View File

@ -19,15 +19,11 @@ import { CoreTestingModule, setupTestBed } from '../../testing';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { UserAccessService } from './user-access.service'; import { UserAccessService } from './user-access.service';
import { JwtHelperService } from './jwt-helper.service'; import { JwtHelperService } from './jwt-helper.service';
import { OAuth2Service } from './oauth2.service';
import { of, throwError } from 'rxjs';
import { userAccessMock } from '../../mock/user-access.mock';
import { AppConfigService } from '../../app-config'; import { AppConfigService } from '../../app-config';
describe('UserAccessService', () => { describe('UserAccessService', () => {
let userAccessService: UserAccessService; let userAccessService: UserAccessService;
let jwtHelperService: JwtHelperService; let jwtHelperService: JwtHelperService;
let oauth2Service: OAuth2Service;
let appConfigService: AppConfigService; let appConfigService: AppConfigService;
setupTestBed({ setupTestBed({
@ -37,13 +33,11 @@ describe('UserAccessService', () => {
beforeEach(() => { beforeEach(() => {
userAccessService = TestBed.inject(UserAccessService); userAccessService = TestBed.inject(UserAccessService);
userAccessService.resetAccess();
jwtHelperService = TestBed.inject(JwtHelperService); jwtHelperService = TestBed.inject(JwtHelperService);
oauth2Service = TestBed.inject(OAuth2Service);
appConfigService = TestBed.inject(AppConfigService); appConfigService = TestBed.inject(AppConfigService);
}); });
function spyUserAccess(realmRoles: string[], resourceAccess: any) { function spyRealmAccess(realmRoles: string[], resourceAccess: any) {
spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token'); spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ spyOn(jwtHelperService, 'decodeToken').and.returnValue({
realm_access: { roles: realmRoles }, realm_access: { roles: realmRoles },
@ -51,131 +45,112 @@ describe('UserAccessService', () => {
}); });
} }
it('should return true when no roles to check are passed in global access', async () => { function spyHxpAuthorization(appkey: string, roles: string[]) {
spyUserAccess(['MOCK_USER_ROLE'], {}); spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token');
await userAccessService.fetchUserAccess(); spyOn(jwtHelperService, 'decodeToken').and.returnValue({
hxp_authorization: {
appkey,
role: roles
}
});
}
it('should return true when no roles to check are passed in global access', () => {
spyRealmAccess(['MOCK_USER_ROLE'], {});
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess([]); const hasGlobalAccess = userAccessService.hasGlobalAccess([]);
expect(hasGlobalAccess).toBe(true); expect(hasGlobalAccess).toBe(true);
}); });
it('should return true when no roles to check are passed in application access', async () => { it('should return true when no roles to check are passed in application access', () => {
spyUserAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } }); spyRealmAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } });
await userAccessService.fetchUserAccess(); userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', []); const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', []);
expect(hasApplicationAccess).toBe(true); expect(hasApplicationAccess).toBe(true);
}); });
describe('Access from JWT token', () => { describe('Access present in realm_access', () => {
it('should return true when the user has one of the global roles', async () => { it('should return true when the user has one of the global roles', () => {
spyUserAccess(['MOCK_USER_ROLE', 'MOCK_USER_ROLE_2'], {}); spyRealmAccess(['MOCK_USER_ROLE', 'MOCK_USER_ROLE_2'], {});
await userAccessService.fetchUserAccess(); userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE']); const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE']);
expect(hasGlobalAccess).toEqual(true); expect(hasGlobalAccess).toEqual(true);
}); });
it('should return true when the user has one of the roles for an application', async () => { it('should return true when the user has one of the roles for an application', () => {
spyUserAccess([], { mockApp: { roles: ['MOCK_APP_ROLE', 'MOCK_APP_ROLE_2'] } }); spyRealmAccess([], { mockApp: { roles: ['MOCK_APP_ROLE', 'MOCK_APP_ROLE_2'] } });
await userAccessService.fetchUserAccess(); userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', ['MOCK_APP_ROLE']); const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', ['MOCK_APP_ROLE']);
expect(hasApplicationAccess).toEqual(true); expect(hasApplicationAccess).toEqual(true);
}); });
it('should return false when the user has none of the global roles', async () => { it('should return false when the user has none of the global roles', () => {
spyUserAccess(['MOCK_USER_ROLE'], {}); spyRealmAccess(['MOCK_USER_ROLE'], {});
await userAccessService.fetchUserAccess(); userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE_2']); const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE_2']);
expect(hasGlobalAccess).toEqual(false); expect(hasGlobalAccess).toEqual(false);
}); });
it('should return false when the user has none of the roles for an application', async () => { it('should return false when the user has none of the roles for an application', () => {
spyUserAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } }); spyRealmAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } });
await userAccessService.fetchUserAccess(); userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', ['MOCK_APP_ROLE_2']); const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', ['MOCK_APP_ROLE_2']);
expect(hasApplicationAccess).toEqual(false); expect(hasApplicationAccess).toEqual(false);
}); });
}); });
describe('Access from the API', () => { describe('Access present in hxp_authorization', () => {
let getAccessFromApiSpy: jasmine.Spy;
beforeEach(() => { it('should return true when the user has one of the global roles', () => {
spyOn(jwtHelperService, 'getValueFromLocalToken').and.returnValue(undefined); spyHxpAuthorization('mockApp1', ['MOCK_GLOBAL_USER_ROLE']);
getAccessFromApiSpy = spyOn(oauth2Service, 'get').and.returnValue(of(userAccessMock)); appConfigService.config = { application: { key: 'mockApp1' } };
appConfigService.config.authType = 'OAUTH'; userAccessService.fetchUserAccess();
});
it('should return true when the user has one of the global roles', async () => {
await userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_GLOBAL_USER_ROLE']); const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_GLOBAL_USER_ROLE']);
expect(hasGlobalAccess).toEqual(true); expect(hasGlobalAccess).toEqual(true);
}); });
it('should return true when the user has one of the roles for an application', async () => { it('should return true when the user has one of the roles for an application', () => {
await userAccessService.fetchUserAccess(); spyHxpAuthorization('mockApp1', ['MOCK_USER_ROLE_APP_1']);
appConfigService.config = { application: { key: 'mockApp1' } };
userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp1', ['MOCK_USER_ROLE_APP_1']); const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp1', ['MOCK_USER_ROLE_APP_1']);
expect(hasApplicationAccess).toEqual(true); expect(hasApplicationAccess).toEqual(true);
}); });
it('should return false when the user has none of the global roles', async () => { it('should return false when the user has none of the global roles', () => {
await userAccessService.fetchUserAccess(); spyHxpAuthorization('mockApp1', ['MOCK_USER_ROLE_APP_1']);
appConfigService.config = { application: { key: 'mockApp1' } };
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE_NOT_EXISTING']); const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE_NOT_EXISTING']);
expect(hasGlobalAccess).toEqual(false); expect(hasGlobalAccess).toEqual(false);
}); });
it('should return false when the user has none of the roles for an application', async () => { it('should return false when the user has none of the roles for an application', () => {
await userAccessService.fetchUserAccess(); spyHxpAuthorization('mockApp1', ['MOCK_USER_ROLE_APP_1']);
appConfigService.config = { application: { key: 'mockApp1' } };
userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp1', ['MOCK_ROLE_NOT_EXISING_IN_APP']); const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp1', ['MOCK_ROLE_NOT_EXISING_IN_APP']);
expect(hasApplicationAccess).toEqual(false); expect(hasApplicationAccess).toEqual(false);
}); });
it('should not call more than once the api to fetch the user access', async () => {
await userAccessService.fetchUserAccess();
await userAccessService.fetchUserAccess();
await userAccessService.fetchUserAccess();
expect(getAccessFromApiSpy.calls.count()).toEqual(1);
}); });
it('should the url be composed from bpm host of app.config', async () => { it('should return false when access is neither in realm_access or hxp_authorization', () => {
const fakeIdentityHost = 'https://fake-identity-host.fake.com'; spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token');
appConfigService.config.bpmHost = fakeIdentityHost; spyOn(jwtHelperService, 'decodeToken').and.returnValue({ mock_access: {} });
await userAccessService.fetchUserAccess(); userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['mock_role']);
expect(getAccessFromApiSpy).toHaveBeenCalledWith({ url: `${fakeIdentityHost}/identity-adapter-service/v1/roles` }); expect(hasGlobalAccess).toEqual(false);
});
it('should the url contain appkey if its present in app config', async () => {
const fakeIdentityHost = 'https://fake-identity-host.fake.com';
appConfigService.config.bpmHost = fakeIdentityHost;
appConfigService.config.application.key = 'fake-app-key';
await userAccessService.fetchUserAccess();
expect(getAccessFromApiSpy).toHaveBeenCalledWith({ url: `${fakeIdentityHost}/identity-adapter-service/v1/roles` , queryParams: { appkey: 'fake-app-key' } });
});
it('should not fetch the access from the API if is not configured with OAUTH', async () => {
appConfigService.config.authType = 'BASIC';
await userAccessService.fetchUserAccess();
expect(getAccessFromApiSpy).not.toHaveBeenCalled();
});
it('should set empty access list on fething roles error', async () => {
getAccessFromApiSpy.and.returnValue(throwError({ status: 503 }));
await userAccessService.fetchUserAccess();
expect(userAccessService.hasGlobalAccess(['MOCKED_ROLES'])).toBe(false);
});
}); });
}); });

View File

@ -18,13 +18,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { JwtHelperService } from './jwt-helper.service'; import { JwtHelperService } from './jwt-helper.service';
import { ApplicationAccessModel } from '../models/application-access.model'; import { ApplicationAccessModel } from '../models/application-access.model';
import { UserAccessModel } from '../models/user-access.model';
import { AppConfigService } from '../../app-config/app-config.service'; import { AppConfigService } from '../../app-config/app-config.service';
import { OAuth2Service } from './oauth2.service';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
const IDENTITY_MICRO_SERVICE_INGRESS = 'identity-adapter-service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -34,60 +28,43 @@ export class UserAccessService {
private applicationAccess: ApplicationAccessModel[]; private applicationAccess: ApplicationAccessModel[];
constructor(private jwtHelperService: JwtHelperService, constructor(private jwtHelperService: JwtHelperService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService) {
private oAuth2Service: OAuth2Service) {
} }
async fetchUserAccess() { fetchUserAccess() {
if (!this.hasFetchedAccess()) { if (this.hasRolesInRealmAccess()) {
if (this.hasRolesInJwt()) { this.fetchAccessFromRealmAccess();
this.fetchAccessFromJwt(); } else if (this.hasRolesInHxpAuthorization()) {
} else if (this.isOauth()) { this.fetchAccessFromHxpAuthorization();
await this.fetchAccessFromApi();
}
} }
} }
private fetchAccessFromJwt() { private fetchAccessFromRealmAccess() {
this.globalAccess = this.jwtHelperService.getValueFromLocalToken<any>(JwtHelperService.REALM_ACCESS).roles; this.globalAccess = this.jwtHelperService.getValueFromLocalToken<any>(JwtHelperService.REALM_ACCESS).roles;
this.applicationAccess = this.jwtHelperService.getValueFromLocalToken<any>(JwtHelperService.RESOURCE_ACCESS); this.applicationAccess = this.jwtHelperService.getValueFromLocalToken<any>(JwtHelperService.RESOURCE_ACCESS);
} }
private async fetchAccessFromApi() { private fetchAccessFromHxpAuthorization() {
const url = `${this.identityHost}/${IDENTITY_MICRO_SERVICE_INGRESS}/v1/roles`; this.globalAccess = [];
const appkey = this.appConfigService.get('application.key'); const hxpAuthorization = this.jwtHelperService.getValueFromLocalToken<any>(JwtHelperService.HXP_AUTHORIZATION);
const opts = appkey ? { url, queryParams: { appkey } } : { url }; if (hxpAuthorization?.appkey && hxpAuthorization?.role) {
this.applicationAccess = [
await this.oAuth2Service.get(opts) {
.pipe( name: hxpAuthorization.appkey,
catchError(() => of({ roles: hxpAuthorization.role
globalAccess: { }
roles: [] ];
}, } else {
applicationAccess: [] this.applicationAccess = [];
})) }
)
.toPromise()
.then((response: UserAccessModel) => {
this.globalAccess = response.globalAccess.roles;
this.applicationAccess = response.applicationAccess;
});
} }
private hasRolesInJwt(): boolean { private hasRolesInRealmAccess(): boolean {
return !!this.jwtHelperService.getValueFromLocalToken(JwtHelperService.REALM_ACCESS); return !!this.jwtHelperService.getValueFromLocalToken(JwtHelperService.REALM_ACCESS);
} }
private hasFetchedAccess(): boolean { private hasRolesInHxpAuthorization(): boolean {
return !!this.globalAccess && !!this.applicationAccess; return !!this.jwtHelperService.getValueFromLocalToken(JwtHelperService.HXP_AUTHORIZATION);
}
private get identityHost(): string {
return `${this.appConfigService.get('bpmHost')}`;
}
private isOauth(): boolean {
return this.appConfigService.get('authType') === 'OAUTH';
} }
/** /**
@ -98,10 +75,21 @@ export class UserAccessService {
*/ */
hasGlobalAccess(rolesToCheck: string[]): boolean { hasGlobalAccess(rolesToCheck: string[]): boolean {
if (rolesToCheck?.length > 0) { if (rolesToCheck?.length > 0) {
if (this.hasRolesInRealmAccess()) {
return this.globalAccess ? this.globalAccess.some((role: string) => rolesToCheck.includes(role)) : false; return this.globalAccess ? this.globalAccess.some((role: string) => rolesToCheck.includes(role)) : false;
} else if (this.hasRolesInHxpAuthorization()) {
return this.isCurrentAppKeyInToken() ? this.applicationAccess[0]?.roles.some((role: string) => rolesToCheck.includes(role)) : false;
} }
} else {
return true; return true;
} }
return false;
}
private isCurrentAppKeyInToken(): boolean {
const currentAppKey = this.appConfigService.get('application.key');
return this.applicationAccess?.length ? currentAppKey === this.applicationAccess[0]?.name : false;
}
/** /**
* Checks for global roles access. * Checks for global roles access.
@ -112,17 +100,9 @@ export class UserAccessService {
*/ */
hasApplicationAccess(appName: string, rolesToCheck: string[]): boolean { hasApplicationAccess(appName: string, rolesToCheck: string[]): boolean {
if (rolesToCheck?.length > 0) { if (rolesToCheck?.length > 0) {
const appAccess = this.hasRolesInJwt() ? this.applicationAccess[appName] : this.applicationAccess.find((app: ApplicationAccessModel) => app.name === appName); const appAccess = this.hasRolesInRealmAccess() ? this.applicationAccess[appName] : this.applicationAccess.find((app: ApplicationAccessModel) => app.name === appName);
return appAccess ? appAccess.roles.some(appRole => rolesToCheck.includes(appRole)) : false; return appAccess ? appAccess.roles.some(appRole => rolesToCheck.includes(appRole)) : false;
} }
return true; return true;
} }
/**
* Resets the cached user access
*/
resetAccess() {
this.globalAccess = undefined;
this.applicationAccess = undefined;
}
} }