I've implemented Module Role conception. Module Role is bound to a Module ID, It's like a unique identifier of Angular app page. A Module Role is filled with ordinary permissions. At the same time, a group of Module Roles can be assigned to an ordinary role. This conception works as expected for per-page access, when a page is accessed via menu, because Module ID is passed to back-end and I can check if there specific page has three requested permissions.
However, it doesn't work properly when I load the app and expect to see only those pages in the menu whose Module ID has the required permission for this page.
So the question is how to pass-through additional information of route (like this module ID) from Angular app to a back-end:
 
 when I load the specific page via URL:
when I load the specific page via URL:
 or just navigating to the app https://localhost:4200 (and expecting to see only specific module ID-related pages in the menu)
to have it available in any possible way (via DI request object, etc.) inside your method:
or just navigating to the app https://localhost:4200 (and expecting to see only specific module ID-related pages in the menu)
to have it available in any possible way (via DI request object, etc.) inside your method:
public override async Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context)
?
26 Answer(s)
- 
    0when you pass the a value via router, you can get the variable with activatedroutehttps://angular.io/api/router/ActivatedRoute If you. want to sent the data all request under the router, you may write a custom api-interceptor. or if you want use the value like requiredPolicy / permission. You may write a guard like. PermissionGuard https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts
- 
    0The mechanism we implemented is an advanced role permission check - with an intermediate layer added. It replaces the standard ABP role mechanism, so would be logically to override client-side permission check instead of adding something new (at back-end we replaced Role Provider). Moreover, we don't want to write a new guard from scratch, because most of the functionality of ABP PermissionGuardfits us.So, is it possible to override PermissionGuardkeeping in mind the following requirement:considering the following route data for "Page 1": { moduleId: "Module 1", requiredPolicy: "Permission 1" }this page needs to be visible in the menu only if server finds "Permission 1" among all permissions assigned to "Module 1"? I see two problems here: - even if I pass the collection of moduleIdpresent in our route (using interceptor or whatever) and I will be able to read this information from request insidepublic override async Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context)- the output parameter signature for this method does not suppose something besides permission. It supposes the permission is either granted or not. In our case, the same permission may be granted to one module id (and then the corresponding page is shown) and revoked to another module id...
- even I manage to adapt a modified grantedPoliciesparameter for the overriden ABPPermissionServiceat client-side (I suppose this service makes decision if the specific page is shown in the menu), I am not sure I will make things work in general, probably you may tell me:  Record<string, boolean>needs to becomeRecord<string + string, boolean>("permission A" is granted or revoked for "module A")
 UPDATE: what I have additionally discovered is that I cannot control ABP pages permissions - the pages which I have not overriden. It's weird or I'm missing something:  Permission check of such pages in custom Permission check of such pages in customPermissionValueProviderdoes not happen, i.e.CheckAsyncis not triggered...
- even if I pass the collection of 
- 
    0The mechanism we implemented is an advanced role permission check - with an intermediate layer added. It replaces the standard ABP role mechanism, so would be logically to override client-side permission check instead of adding something new (at back-end we replaced Role Provider). Moreover, we don't want to write a new guard from scratch, because most of the functionality of ABP PermissionGuardfits us.So, is it possible to override PermissionGuardkeeping in mind the following requirement:considering the following route data for "Page 1": { moduleId: "Module 1", requiredPolicy: "Permission 1" }this page needs to be visible in the menu only if server finds "Permission 1" among all permissions assigned to "Module 1"? I see two problems here: - even if I pass the collection of moduleIdpresent in our route (using interceptor or whatever) and I will be able to read this information from request insidepublic override async Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context)- the output parameter signature for this method does not suppose something besides permission. It supposes the permission is either granted or not. In our case, the same permission may be granted to one module id (and then the corresponding page is shown) and revoked to another module id...
- even I manage to adapt a modified grantedPoliciesparameter for the overriden ABPPermissionServiceat client-side (I suppose this service makes decision if the specific page is shown in the menu), I am not sure I will make things work in general, probably you may tell me:  Record<string, boolean>needs to becomeRecord<string + string, boolean>("permission A" is granted or revoked for "module A")
 UPDATE: what I have additionally discovered is that I cannot control ABP pages permissions - the pages which I have not overriden. It's weird or I'm missing something:  Permission check of such pages in custom Permission check of such pages in customPermissionValueProviderdoes not happen, i.e.CheckAsyncis not triggered...Cou can override permission.guard.ts with Angular DI. See the example https://gist.github.com/mahmut-gundogdu/24b8000310daa940c2d91e83c6f77658 
- even if I pass the collection of 
- 
    0for the problem two. I would store with service and you can read it with injection 
- 
    0Thank you for reply. However I've just checked out that canActivateis not triggered when I reload my root page, i.e. it is not responsible for showing/hiding menu items:Question 1) what ABP service/method is actually used to decide what to show or hide in the menu? Question 2) meanwhile to save time while waiting for your reply I decided to make a server method (the result is supposed to be cached in Angular client) which returns mapping between module IDs and their permissions. Is it a correct approach? I've tried to use PermissionAppService.GetAsync()directly inside the loop, but it is very slow:public virtual async Task<Dictionary<string, IEnumerable<string>>> GetModuleRolePermissionMapAsync() { var moduleRoleNames = await _modulePermissionChecker.GetModuleRoleNamesAsync(CurrentTenant.Id, CurrentUser.Id, CurrentUser.IsAuthenticated); var result = new Dictionary<string, IEnumerable<string>>(); using (CurrentTenant.Change(null)) { var permissionGrants = await _permissionGrantRepository.GetListAsync(); result = moduleRoleNames.GroupJoin ( permissionGrants.Where(pg => pg.ProviderName == RolePermissionValueProvider.ProviderName), moduleRoleName => moduleRoleName, permissionGrant => permissionGrant.ProviderKey, (moduleRoleName, permissionGrants) => (moduleRoleName, permissionGrantNames: permissionGrants.Select(x => x.Name)) ) .ToDictionary(x => x.moduleRoleName, x => x.permissionGrantNames); } return result; }
- 
    0Hello Alexander, For the customize guard system you can customize PermissionGuard like below //custom-permission.guard.ts import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable, of, switchMap, tap } from 'rxjs'; import { PermissionGuard } from '@abp/ng.core'; @Injectable({ providedIn: 'root', }) export class CustomPermissionGuard extends PermissionGuard { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { let { moduleId } = route.data || {}; return super.canActivate(route, state).pipe( switchMap(access => { if (!access) { return of(false); } if (!moduleId) { return of(true); } //Custom logic return of(true); }), tap(access => { if (!access && this.oAuthService.hasValidAccessToken()) { this.httpErrorReporter.reportError({ status: 403 } as HttpErrorResponse); } }), ); } }//app.module.ts import { PermissionGuard } from '@abp/ng.core'; import { CustomPermissionGuard } from './custom-permission.guard'; @NgModule({ imports: [], providers: [ { provide: PermissionGuard, useExisting: CustomPermissionGuard, }, ], declarations: [AppComponent], bootstrap: [AppComponent], }) export class AppModule {}//app-routing.module.ts const routes: Routes = [ { path: 'identity', loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()), data: { moduleId: 'Identity', }, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes, {})], exports: [RouterModule], }) export class AppRoutingModule {}
- 
    0I heard the idea of Permission Guard earlier in this thread and I wrote about it in my previous message: However I've just checked out that canActivate is not triggered when I reload my root page I.e. when I navigate to the root page (https://localhost:4200/) of the site (not a specific page) - this guard is not triggered. And the site menu is not checked against available module ID permissions, so all the pages are shown, even those which need to be hidden according to the available module ID permissions. So I asked what needs to be overriden in the given case - definitely it is not a Permission Guard. Actually I do not need "403" error if there is no permission: this is already handled by back-end method public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)What I need now is to HIDE the page if there is no permission assigned to its module ID. I prepared API call with module ID mappings which returns the following structure: "moduleID1" => ["permissionA", "permissionX"], "moduleID2" => ["permissionY", "permissionX"], If the page has moduleId="moduleID1" and "permissionB" as a requiredPolicy, it needs to be hidden from the menu, since mapping for "moduleID1" does not contain "permissionB"So I just need to know what is ABP code where I can put this check. Please, reply ASAP. Probably it needs to happen inside this.route.visible$.subscribe(visibleRoute => { ... })ofapp.component.ts, but I need to be sure there is one and only one place which controls menu item visibility and it does not make conflict with other relevant ABP code.UPDATE: here is another problem - the ABP getters below do not store dataattribute, so they do not return this information:this.route.flat$.subscribe((flatRoute: ABP.Route[]) => { //PROBLEM: flatRoute does not contain `data`, so we cannot retrieve `moduleId` flatRoute.filter(x => x.requiredPolicy).forEach(routeItemWithPolicy => { console.log(routeItemWithPolicy); });
- 
    0I heard the idea of Permission Guard earlier in this thread and I wrote about it in my previous message: However I've just checked out that canActivate is not triggered when I reload my root page Hi, You can still use the RoutesService to modify the menu anytime and anywhere in the app you can use it in the app.component.ts. https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu#how-to-patch-or-remove-a-navigation-element 
- 
    0You can still use the RoutesService to modify the menu anytime and anywhere in the app you can use it in the app.component.ts. Hi. I am aware about this. But what I actually need is to retrieve information about dataof my route items. As I mentioned,RoutersServicedoes not provide this information.
- 
    0
- 
    0The problem is that this.router.configdoes not return lazy-loading routing module nodes where actually my data withmoduleIdresides. It only returns the ordinary nodes.
- 
    0Hi, I am checking on this do you need a workaround? 
- 
    0I need to receive all nodes which are present in the menu, both lazy and non-lazy. And obtain dataproperty of each node. AngularRouterconfig DOES return data, but it does not return lazy nodes. ABPRoutesServiceflat$ / tree$ / visible$ DOES return all nodes, but it does not returndata. So I need something which would return BOTH.
- 
    0
- 
    0Hmm, probably it would work this way. I would give it a try. However, I just discovered I was not right in my suggestions. ABP RoutesServicedoes not return all nodes in the menu - it returns only non-lazy nodes. So what I need is to load a whole structure of my menu into a variable and then check each node module ID inside the loop. The question is - what is a proper way to load all the nodes? Are there ready ABP methods for it? Or I need to use Angular methods? Or I need to create my own recursive method? I know it won't be complex, but don't want to re-invent a wheel...
- 
    0
- 
    0
- 
    0Hi, Can i know what challenges are you facing when using RoutersService? 
- 
    0Hi. Seems like I have eventually nailed it. My steps were: - adding - moduleIdto non-lazy node collection in- route.provider.ts(I copied the corresponding values from lazy-loading nodes (- path: ''in the routing module). I applied your- as anyrecipe, thank you;
- returning the following multiple permission result from back-end to be able to handle (hide) nodes in Angular app using mapping from back-end: - public override Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context) { var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToList(); Check.NotNullOrEmpty(permissionNames, nameof(permissionNames)); return Task.FromResult(new MultiplePermissionGrantResult(permissionNames.ToArray(), PermissionGrantResult.Granted)); }
- making API request retrieving the mapping between - moduleId(or all simple roles) and their permissions for a current user (modulePermissionMap$);
- filtering out all the menu items which need to be hidden based on modulePermissionMap$, - RoutersService.flat$(combineLatest) and eventually applying- RoutersService.removemethod;
 I will keep the ticket opened so far - in case our testing team finds something wrong during the nearest time... 
- 
    0Anjali, do you know where should I put my "hide" method? The small issue now is that initially the menu is shown fully - pause for API mapping call - and then it is filtered out accordingly. I want the menu was shown after it has been filtered out... this.currentUser$.subscribe(currentUser => { if (currentUser?.isAuthenticated) { ... this.modulePermissionHandler.init(); // it is too late :( ... });I've tried to replace RoutesService:export class AbxRoutesService extends AbstractNavTreeService<ABP.Route>and make filtration inside public override createTree(items: ABP.Route[]): TreeNode<ABP.Route>[]But this method is synchronous and I cannot await mapping API call result. On other hand, I don't want to do something about flat$getter, because the filtration needs to be done just before building the tree.It is so frustrating... 
- 
    0So far I have not found a better approach than hiding (setting invisibleto an item withrequiredPolicyandmoduleId) in thecreateTreemethod) the items prior to removing or showing them inside subcription ofcombineLatest.UPDATE: i've checked this approach and it has some drawbacks - after filtering the menu structure is broken (some intermediate parent nodes disappeared) and I cannot navigate to the displayed pages with "no matching route" error. I don't know if I am doing something wrong or it is bad idea to override RoutesServiceand hide the items in thecreateTreecall.
- 
    0OK - so I abandoned the idea to override the RoutesServiceand implemented the following handler (retaining the idea to hide the elements ASAP and unhide them after getting service data) - please have a look if there could be a more efficient solution.import { ABP, RoutesService } from '@abp/ng.core'; import { Inject, Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject, ReplaySubject, Subscription, combineLatest } from 'rxjs'; import { distinctUntilChanged, filter, takeLast, takeUntil } from 'rxjs/operators'; import { ModulePermissionsService } from '../services/module-permissions.service'; @Injectable({ providedIn: 'root' }) export class ModulePermissionHandler implements OnDestroy { modulePermissionMap$: BehaviorSubject<{[key: string]: string[]}> = new BehaviorSubject<{[key: string]: string[]}>({}); private destroy: ReplaySubject<any> = new ReplaySubject<any>(1); private stopMenuScan$: ReplaySubject<any> = new ReplaySubject<any>(1); private mapBindingComplete: boolean = false; constructor( private routesService: RoutesService, private modulePermissionsService: ModulePermissionsService ) { } ngOnDestroy(): void { this.deinit(); } init() { this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true); this.routesService.refresh(); combineLatest([this.modulePermissionMap$, this.routesService.flat$]) .pipe ( filter(result => !this.mapBindingComplete && Object.keys(result[0]).length > 0 && Object.keys(result[1]).length > 0), takeUntil(this.destroy), takeUntil(this.stopMenuScan$) ) .subscribe(([modulePermissionMap, nonLazyLoadedRoute]) => { let permissionProhibitedPageIds: string[] = []; nonLazyLoadedRoute.filter(node => node.requiredPolicy).forEach((nonLazyRouteItem: ABP.Route) => { let moduleId = (nonLazyRouteItem as any).data?.moduleId; if (moduleId) { const moduleIdPolicyViolated = !modulePermissionMap[moduleId] || modulePermissionMap[moduleId] && !modulePermissionMap[moduleId].includes(nonLazyRouteItem.requiredPolicy as string); const ordinaryRolePolicyViolated = !modulePermissionMap['_ordinaryRole'] || modulePermissionMap['_ordinaryRole'] && !modulePermissionMap['_ordinaryRole'].includes(nonLazyRouteItem.requiredPolicy as string); if (moduleIdPolicyViolated && ordinaryRolePolicyViolated) { permissionProhibitedPageIds.push(nonLazyRouteItem.name); } else { nonLazyRouteItem.invisible = false; } } }); this.stopMenuScan$.next(null); this.stopMenuScan$.complete(); this.routesService.remove(permissionProhibitedPageIds); this.mapBindingComplete = true; this.stopMenuScan$ = new ReplaySubject(1); }); this.modulePermissionsService.getModulePermissionMap() .pipe(takeUntil(this.destroy)) .subscribe(modulePermissionMap => { this.mapBindingComplete = false; this.modulePermissionMap$.next(modulePermissionMap); }); } deinit() { this.destroy.next(null); this.destroy.complete(); } }Usage - app.component.ts: ngOnInit(): void { this.oAuthService.events .pipe(filter(event => event?.type === 'logout')) .subscribe(() => { this.modulePermissionHandler.deinit(); }); this.currentUser$.subscribe(currentUser => { if (currentUser?.isAuthenticated) { this.modulePermissionHandler.init(); } }); }And - yes - I had also to override PermissionGuard: I have copy-pasted this from route.provider.ts to app-routing.module.ts: and check and checkthis.modulePermissionMap$inCanActivatemethod.
- 
    0Hi, I will check and let you know. Thank you. 
- 
    0Ok. Waiting. 
- 
    0Hi, The module with permission information should be included in the application-configuration AbpApplicationConfigurationAppService.cs After making a change. Please inject ConfigStateService and get module permission related data like this. this way there will be no delay. 












 
                                