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

View File

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

View File

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

View File

@ -19,15 +19,11 @@ import { CoreTestingModule, setupTestBed } from '../../testing';
import { TestBed } from '@angular/core/testing';
import { UserAccessService } from './user-access.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';
describe('UserAccessService', () => {
let userAccessService: UserAccessService;
let jwtHelperService: JwtHelperService;
let oauth2Service: OAuth2Service;
let appConfigService: AppConfigService;
setupTestBed({
@ -37,13 +33,11 @@ describe('UserAccessService', () => {
beforeEach(() => {
userAccessService = TestBed.inject(UserAccessService);
userAccessService.resetAccess();
jwtHelperService = TestBed.inject(JwtHelperService);
oauth2Service = TestBed.inject(OAuth2Service);
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, 'decodeToken').and.returnValue({
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 () => {
spyUserAccess(['MOCK_USER_ROLE'], {});
await userAccessService.fetchUserAccess();
function spyHxpAuthorization(appkey: string, roles: string[]) {
spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token');
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([]);
expect(hasGlobalAccess).toBe(true);
});
it('should return true when no roles to check are passed in application access', async () => {
spyUserAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } });
await userAccessService.fetchUserAccess();
it('should return true when no roles to check are passed in application access', () => {
spyRealmAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } });
userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', []);
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 () => {
spyUserAccess(['MOCK_USER_ROLE', 'MOCK_USER_ROLE_2'], {});
await userAccessService.fetchUserAccess();
it('should return true when the user has one of the global roles', () => {
spyRealmAccess(['MOCK_USER_ROLE', 'MOCK_USER_ROLE_2'], {});
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE']);
expect(hasGlobalAccess).toEqual(true);
});
it('should return true when the user has one of the roles for an application', async () => {
spyUserAccess([], { mockApp: { roles: ['MOCK_APP_ROLE', 'MOCK_APP_ROLE_2'] } });
await userAccessService.fetchUserAccess();
it('should return true when the user has one of the roles for an application', () => {
spyRealmAccess([], { mockApp: { roles: ['MOCK_APP_ROLE', 'MOCK_APP_ROLE_2'] } });
userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', ['MOCK_APP_ROLE']);
expect(hasApplicationAccess).toEqual(true);
});
it('should return false when the user has none of the global roles', async () => {
spyUserAccess(['MOCK_USER_ROLE'], {});
await userAccessService.fetchUserAccess();
it('should return false when the user has none of the global roles', () => {
spyRealmAccess(['MOCK_USER_ROLE'], {});
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE_2']);
expect(hasGlobalAccess).toEqual(false);
});
it('should return false when the user has none of the roles for an application', async () => {
spyUserAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } });
await userAccessService.fetchUserAccess();
it('should return false when the user has none of the roles for an application', () => {
spyRealmAccess([], { mockApp: { roles: ['MOCK_APP_ROLE'] } });
userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp', ['MOCK_APP_ROLE_2']);
expect(hasApplicationAccess).toEqual(false);
});
});
describe('Access from the API', () => {
let getAccessFromApiSpy: jasmine.Spy;
describe('Access present in hxp_authorization', () => {
beforeEach(() => {
spyOn(jwtHelperService, 'getValueFromLocalToken').and.returnValue(undefined);
getAccessFromApiSpy = spyOn(oauth2Service, 'get').and.returnValue(of(userAccessMock));
appConfigService.config.authType = 'OAUTH';
});
it('should return true when the user has one of the global roles', async () => {
await userAccessService.fetchUserAccess();
it('should return true when the user has one of the global roles', () => {
spyHxpAuthorization('mockApp1', ['MOCK_GLOBAL_USER_ROLE']);
appConfigService.config = { application: { key: 'mockApp1' } };
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_GLOBAL_USER_ROLE']);
expect(hasGlobalAccess).toEqual(true);
});
it('should return true when the user has one of the roles for an application', async () => {
await userAccessService.fetchUserAccess();
it('should return true when the user has one of the roles for an application', () => {
spyHxpAuthorization('mockApp1', ['MOCK_USER_ROLE_APP_1']);
appConfigService.config = { application: { key: 'mockApp1' } };
userAccessService.fetchUserAccess();
const hasApplicationAccess = userAccessService.hasApplicationAccess('mockApp1', ['MOCK_USER_ROLE_APP_1']);
expect(hasApplicationAccess).toEqual(true);
});
it('should return false when the user has none of the global roles', async () => {
await userAccessService.fetchUserAccess();
it('should return false when the user has none of the global roles', () => {
spyHxpAuthorization('mockApp1', ['MOCK_USER_ROLE_APP_1']);
appConfigService.config = { application: { key: 'mockApp1' } };
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['MOCK_USER_ROLE_NOT_EXISTING']);
expect(hasGlobalAccess).toEqual(false);
});
it('should return false when the user has none of the roles for an application', async () => {
await userAccessService.fetchUserAccess();
it('should return false when the user has none of the roles for an application', () => {
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']);
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();
it('should return false when access is neither in realm_access or hxp_authorization', () => {
spyOn(jwtHelperService, 'getAccessToken').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ mock_access: {} });
userAccessService.fetchUserAccess();
const hasGlobalAccess = userAccessService.hasGlobalAccess(['mock_role']);
expect(getAccessFromApiSpy.calls.count()).toEqual(1);
});
it('should the url be composed from bpm host of app.config', async () => {
const fakeIdentityHost = 'https://fake-identity-host.fake.com';
appConfigService.config.bpmHost = fakeIdentityHost;
await userAccessService.fetchUserAccess();
expect(getAccessFromApiSpy).toHaveBeenCalledWith({ url: `${fakeIdentityHost}/identity-adapter-service/v1/roles` });
});
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);
});
expect(hasGlobalAccess).toEqual(false);
});
});

View File

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