load permissions when user is loaded

This commit is contained in:
Dan Percic 2022-10-17 14:34:17 +03:00
parent 7d4e0a851a
commit 843415d9fd
5 changed files with 80 additions and 92 deletions

View File

@ -12,11 +12,11 @@
<ng-container *ngIf="searchPosition === searchPositions.beforeFilters" [ngTemplateOutlet]="searchBar"></ng-container>
<ng-container *ngFor="let config of filters; trackBy: trackByLabel">
<ng-container *ngFor="let filter of filters; trackBy: trackByLabel">
<iqser-popup-filter
*ngIf="!config.hide"
*ngIf="!filter.hide"
[iqserHelpMode]="filterHelpModeKey"
[primaryFiltersSlug]="config.slug"
[primaryFiltersSlug]="filter.slug"
></iqser-popup-filter>
</ng-container>

View File

@ -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<unknown>;
@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<unknown>,
) {
this.#thenTemplateRef = templateRef;
@ -64,7 +64,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
}
@Input()
set allowThen(template: NgTemplate) {
set allowThen(template: TemplateRef<unknown>) {
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<unknown>) {
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<unknown>) {
this._viewContainer.clear();
return template ? this._viewContainer.createEmbeddedView(template) : undefined;
return template ? this._viewContainer.createEmbeddedView(template) : true;
}
}

View File

@ -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<RedirectToFn>(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<RedirectToFn>(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<RedirectToFn>(permissionRedirectTo)
? permissionRedirectTo(failedPermissionName, route, state)
: permissionRedirectTo;
const _redirectTo = isFunction<RedirectToFn>(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<NavigationCommandsFn>(navigationCommands)
? navigationCommands(route, state)
: navigationCommands;
let navigationExtras = _redirectTo.navigationExtras ?? {};
navigationExtras = isFunction<NavigationExtrasFn>(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<NavigationCommandsFn>(navigationCommands) ? navigationCommands(route, state) : navigationCommands;
}
#transformNavigationExtras(
navigationExtras: NavigationExtras | NavigationExtrasFn,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): NavigationExtras {
return isFunction<NavigationExtrasFn>(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<boolean> {
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;
});
}
}

View File

@ -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<boolean>;
has(permissions: List): Promise<boolean>;
has(permissions: string | List): Promise<boolean>;
has(value: string | List): Promise<boolean> {
const isEmpty = !value || value.length === 0;
return isEmpty ? Promise.resolve(true) : this.#has(toArray(value));
has(permissions: string | List): Promise<boolean> {
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<boolean>;
has$(permissions: List): Observable<boolean>;
has$(permissions: string | List): Observable<boolean>;
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<boolean> {
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));
}
}

View File

@ -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<Class | undefined> {
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);