From 843415d9fd98c75fd16e75e401fefa58402a057d Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 17 Oct 2022 14:34:17 +0300 Subject: [PATCH] load permissions when user is loaded --- .../page-header/page-header.component.html | 6 +- src/lib/permissions/permissions.directive.ts | 18 ++-- .../services/permissions-guard.service.ts | 93 ++++++------------- .../services/permissions.service.ts | 35 ++++--- src/lib/users/services/iqser-user.service.ts | 20 +++- 5 files changed, 80 insertions(+), 92 deletions(-) diff --git a/src/lib/listing/page-header/page-header.component.html b/src/lib/listing/page-header/page-header.component.html index 51f77af..942a874 100644 --- a/src/lib/listing/page-header/page-header.component.html +++ b/src/lib/listing/page-header/page-header.component.html @@ -12,11 +12,11 @@ - + diff --git a/src/lib/permissions/permissions.directive.ts b/src/lib/permissions/permissions.directive.ts index b18fc22..f67aba3 100644 --- a/src/lib/permissions/permissions.directive.ts +++ b/src/lib/permissions/permissions.directive.ts @@ -1,4 +1,5 @@ import { + ChangeDetectorRef, Directive, EmbeddedViewRef, EventEmitter, @@ -18,8 +19,6 @@ import { IqserRolesService } from './services/roles.service'; import { IqserPermissionsService } from './services/permissions.service'; import { List } from '../utils'; -type NgTemplate = TemplateRef; - @Directive({ selector: '[allow]', }) @@ -51,6 +50,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { private readonly _permissionsService: IqserPermissionsService, private readonly _rolesService: IqserRolesService, private readonly _viewContainer: ViewContainerRef, + private readonly _changeDetector: ChangeDetectorRef, templateRef: TemplateRef, ) { this.#thenTemplateRef = templateRef; @@ -64,7 +64,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { } @Input() - set allowThen(template: NgTemplate) { + set allowThen(template: TemplateRef) { assertTemplate('allowThen', template); this.#thenTemplateRef = template; this.#thenViewRef = false; @@ -72,7 +72,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { } @Input() - set allowElse(template: NgTemplate) { + set allowElse(template: TemplateRef) { assertTemplate('allowElse', template); this.#elseTemplateRef = template; this.#elseViewRef = false; @@ -111,7 +111,8 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { this.permissionsUnauthorized.emit(); this.#thenViewRef = false; - this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef) ?? true; + this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef); + this._changeDetector.markForCheck(); } #showThenBlock() { @@ -121,12 +122,13 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { this.permissionsAuthorized.emit(); this.#elseViewRef = false; - this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef) ?? true; + this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef); + this._changeDetector.markForCheck(); } - #showTemplate(template?: NgTemplate) { + #showTemplate(template?: TemplateRef) { this._viewContainer.clear(); - return template ? this._viewContainer.createEmbeddedView(template) : undefined; + return template ? this._viewContainer.createEmbeddedView(template) : true; } } diff --git a/src/lib/permissions/services/permissions-guard.service.ts b/src/lib/permissions/services/permissions-guard.service.ts index c8e9db3..9d8cf59 100644 --- a/src/lib/permissions/services/permissions-guard.service.ts +++ b/src/lib/permissions/services/permissions-guard.service.ts @@ -1,14 +1,5 @@ import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - CanActivate, - CanActivateChild, - CanLoad, - NavigationExtras, - Route, - Router, - RouterStateSnapshot, -} from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router'; import { firstValueFrom, forkJoin, from, of } from 'rxjs'; import { first, mergeMap, tap } from 'rxjs/operators'; @@ -52,71 +43,53 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC return this.#checkPermissions(route); } - #validate(permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) { - if (isFunction(permissions.redirectTo) || !isRedirectWithParameters(permissions.redirectTo)) { - return this.#checkRedirect(permissions, route, state); - } - - return this.#validatePermissions(permissions, route, state); - } - #checkPermissions(route: IqserActivatedRouteSnapshot | IqserRoute, state?: RouterStateSnapshot) { const routePermissions = route.data?.permissions; if (!routePermissions) { - return Promise.resolve(true); + return true; } const permissions = transformPermission(routePermissions, route, state); if (permissions.allow?.length > 0) { - return this.#validate(permissions, route, state); + if (isFunction(permissions.redirectTo) || !isRedirectWithParameters(permissions.redirectTo)) { + return this.#checkRedirect(permissions, route, state); + } + + return this.#validatePermissions(permissions, route, state); } - return Promise.resolve(true); + return true; } #redirectToAnotherRoute( - permissionRedirectTo: RedirectTo | RedirectToFn, + redirectTo: RedirectTo | RedirectToFn, route: ActivatedRouteSnapshot | Route, failedPermissionName: string, state?: RouterStateSnapshot, ) { - const redirectTo = isFunction(permissionRedirectTo) - ? permissionRedirectTo(failedPermissionName, route, state) - : permissionRedirectTo; + const _redirectTo = isFunction(redirectTo) ? redirectTo(failedPermissionName, route, state) : redirectTo; - if (isRedirectWithParameters(redirectTo)) { - redirectTo.navigationCommands = this.#transformNavigationCommands(redirectTo.navigationCommands, route, state); - redirectTo.navigationExtras = this.#transformNavigationExtras(redirectTo.navigationExtras ?? {}, route, state); - return this._router.navigate(redirectTo.navigationCommands, redirectTo.navigationExtras); + if (isRedirectWithParameters(_redirectTo)) { + let navigationCommands = _redirectTo.navigationCommands; + navigationCommands = isFunction(navigationCommands) + ? navigationCommands(route, state) + : navigationCommands; + let navigationExtras = _redirectTo.navigationExtras ?? {}; + navigationExtras = isFunction(navigationExtras) ? navigationExtras(route, state) : navigationExtras; + return this._router.navigate(navigationCommands, navigationExtras); } - if (Array.isArray(redirectTo)) { - return this._router.navigate(redirectTo); + if (Array.isArray(_redirectTo)) { + return this._router.navigate(_redirectTo); } - return this._router.navigate([redirectTo]); - } - - #transformNavigationCommands( - navigationCommands: any[] | NavigationCommandsFn, - route: ActivatedRouteSnapshot | Route, - state?: RouterStateSnapshot, - ) { - return isFunction(navigationCommands) ? navigationCommands(route, state) : navigationCommands; - } - - #transformNavigationExtras( - navigationExtras: NavigationExtras | NavigationExtrasFn, - route: ActivatedRouteSnapshot | Route, - state?: RouterStateSnapshot, - ): NavigationExtras { - return isFunction(navigationExtras) ? navigationExtras(route, state) : navigationExtras; + return this._router.navigate([_redirectTo]); } #checkRedirect(permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) { if (!permissions.allow || permissions.allow.length === 0) { - return Promise.resolve(true); + return true; } let failedPermission = ''; @@ -205,27 +178,21 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC } #validatePermissions( - purePermissions: IqserPermissionsData, + permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot, ): Promise { - const permissions: IqserPermissionsData = { - ...purePermissions, - }; + const results = Promise.all([this._permissionsService.has(permissions.allow), this._rolesService.has(permissions.allow)]); - return Promise.all([this._permissionsService.has(permissions.allow), this._rolesService.has(permissions.allow)]).then( - ([hasPermission, hasRole]) => { - if (hasPermission || hasRole) { - return true; - } - - if (permissions.redirectTo) { + return results + .then(([hasPermission, hasRole]) => hasPermission || hasRole) + .then(isAllowed => { + if (!isAllowed && permissions.redirectTo) { const redirect = this.#redirectToAnotherRoute(permissions.redirectTo, route, permissions.allow[0], state); return redirect.then(() => false); } - return false; - }, - ); + return isAllowed; + }); } } diff --git a/src/lib/permissions/services/permissions.service.ts b/src/lib/permissions/services/permissions.service.ts index e0ba415..b2e3eb7 100644 --- a/src/lib/permissions/services/permissions.service.ts +++ b/src/lib/permissions/services/permissions.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Observable, of, switchMap } from 'rxjs'; import { isArray, isString, toArray } from '../utils'; import { IqserPermissions, PermissionValidationFn } from '../types'; import { List } from '../../utils'; +import { map } from 'rxjs/operators'; @Injectable() export class IqserPermissionsService { @@ -22,9 +23,8 @@ export class IqserPermissionsService { has(permission: string): Promise; has(permissions: List): Promise; has(permissions: string | List): Promise; - has(value: string | List): Promise { - const isEmpty = !value || value.length === 0; - return isEmpty ? Promise.resolve(true) : this.#has(toArray(value)); + has(permissions: string | List): Promise { + return firstValueFrom(this.has$(permissions)); } load(permissions: IqserPermissions | List) { @@ -43,11 +43,29 @@ export class IqserPermissionsService { } get(): IqserPermissions; + get(permission: string): PermissionValidationFn | undefined; + get(permission?: string): IqserPermissions | PermissionValidationFn | undefined { return permission ? this.#permissions$.value[permission] : this.#permissions$.value; } + has$(permission: string): Observable; + has$(permissions: List): Observable; + has$(permissions: string | List): Observable; + has$(permissions: string | List) { + const isEmpty = !permissions || permissions.length === 0; + if (isEmpty) { + return of(true); + } + + return this.permissions$.pipe( + map(all => toArray(permissions).map(permission => all[permission]?.(permission, all) ?? false)), + switchMap(promises => Promise.all(promises)), + map(results => results.every(result => result)), + ); + } + #reduce(permissions: IqserPermissions | List, initialValue = {} as IqserPermissions) { if (isArray(permissions)) { return this.#permissions$.next(this.#reduceList(permissions, initialValue)); @@ -67,13 +85,4 @@ export class IqserPermissionsService { return { ...acc, [permission]: () => true }; }, initialValue); } - - #has(permissions: List): Promise { - const promises = permissions.map(permission => { - const validationFn = this.#permissions$.value[permission]; - return validationFn?.(permission, this.#permissions$.value) ?? false; - }); - - return Promise.all(promises).then(results => results.every(result => result)); - } } diff --git a/src/lib/users/services/iqser-user.service.ts b/src/lib/users/services/iqser-user.service.ts index 44640b5..befbf72 100644 --- a/src/lib/users/services/iqser-user.service.ts +++ b/src/lib/users/services/iqser-user.service.ts @@ -14,6 +14,12 @@ import { IMyProfileUpdateRequest } from '../types/my-profile-update.request'; import { IProfileUpdateRequest } from '../types/profile-update.request'; import { KeycloakProfile } from 'keycloak-js'; import { IqserUser } from '../iqser-user.model'; +import { IqserPermissionsService, IqserRolesService } from '../../permissions'; + +interface IToken { + sub: string; + resource_access: { account: { roles: List }; redaction: { roles: List } }; +} @Injectable() export abstract class IqserUserService< @@ -28,6 +34,8 @@ export abstract class IqserUserService< protected readonly _baseHref = inject(BASE_HREF); protected readonly _keycloakService = inject(KeycloakService); protected readonly _cacheApiService = inject(CacheApiService); + protected readonly _permissionsService = inject(IqserPermissionsService); + protected readonly _rolesService = inject(IqserRolesService); constructor() { super(); @@ -65,11 +73,6 @@ export abstract class IqserUserService< } async loadCurrentUser(): Promise { - const token = await this._keycloakService.getToken(); - const decoded = jwt_decode(token); - const userId = (<{ sub: string }>decoded).sub; - - const roles = this._keycloakService.getUserRoles(true).filter(role => this._rolesFilter(role)); let profile; try { profile = await this._keycloakService.loadUserProfile(true); @@ -79,6 +82,13 @@ export abstract class IqserUserService< return; } + const token = await this._keycloakService.getToken(); + const decoded: IToken = jwt_decode(token); + const userId = decoded.sub; + this._permissionsService.load(decoded.resource_access.redaction.roles); + + const roles = this._keycloakService.getUserRoles(true).filter(role => this._rolesFilter(role)); + this._rolesService.load(roles); const user = new this._entityClass(profile, roles, userId); this.replace(user);