4.3.0 / Angular / separate IdentityServer
I have custom implementation of IAuthorizationMiddlewareResultHandler
(it checks missing permissions for a user) and I want to provide additional exception info from it to Angular client in pop-up window. It works in some scenarios - like the page is accessed from navigation menu and it misses some permission. But if I try to access the page which is not accessible via typing its address in a browser - I ALWAYS get the following page without details:
Ah, OK. What about ABP Permission Grant Cache? Should I use Redis to share one cache between two apps (hosted as separate IIS apps eventually) (or my approach - exchanging messages via bus and refreshing the cache in dependent apps?)
Please specify where I broke ABP design: both applications 1 and applications 2 - are ABP-framework-based. Permissions are set in application 1, since it's a "main" application. The issue was that the permissions set in application 1 were not applied to application 2. I found "workaround", but my question in the first place would be: "Why they are not applied to application 2? They are not applied even if a distributed cache key is not set in both applications. Is it because the cache in both applications is separate? Is using Redis cache a way to go then?"
I understood my mistake in the code: I am missing setting current tenant before changing cache. I am going to send tenant GUID from RabbitMq sender.
Anyway, here's the override of PermissionAppService
in back-end 1:
using Volo.Abp.PermissionManagement;
using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions;
using Microsoft.Extensions.Options;
using System.Linq;
using Volo.Abp.DependencyInjection;
using AbxEps.RabbitMq.Client;
using AbxEps.RabbitMq.Client.Messages;
using AbxEps.Fines;
namespace AbxEps.CentralTools
{
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IPermissionAppService))]
public class CentralToolsPermissionAppService : PermissionAppService
{
private readonly IRabbitMqManager _rabbitMqManager;
public CentralToolsPermissionAppService(
IPermissionManager permissionManager,
IPermissionDefinitionManager permissionDefinitionManager,
IOptions<PermissionManagementOptions> options,
IPermissionStateManager permissionStateManager,
IRabbitMqManager rabbitMqManager)
: base(permissionManager, permissionDefinitionManager, options, permissionStateManager)
{
_rabbitMqManager = rabbitMqManager;
}
public override async Task UpdateAsync(string providerName, string providerKey, UpdatePermissionsDto input)
{
await base.UpdateAsync(providerName, providerKey, input);
var cacheRabbitMqInput = new CacheRabbitMqInput(input.Permissions.Select(x => x)
.ToDictionary(x => PermissionGrantCacheItem.CalculateCacheKey(x.Name, providerName, providerKey), x => x.IsGranted));
await _rabbitMqManager.CreateSender().SendAsync(new RabbitMqMessage<CacheRabbitMqInput>
{
RoutingKey = "AbxEps-Abp-Caching",
Body = cacheRabbitMqInput
});
}
}
}
Here's the triggered RabbitMq subscription in back-end 2:
using Volo.Abp.PermissionManagement;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using System.Collections.Generic;
using AbxEps.RabbitMq.Client.Receivers;
using RabbitMQ.Client.Events;
using System;
using Microsoft.Extensions.DependencyInjection;
using AbxEps.RabbitMq.Client.Extensions;
using System.Linq;
namespace AbxEps.Fines
{
public class FinesRabbitMqReceiver : RabbitMqReceiverBase
{
private readonly IServiceProvider _serviceProvider;
public FinesRabbitMqReceiver(IServiceProvider serviceProvider) : base("AbxEps-Abp-Caching")
{
_serviceProvider = serviceProvider;
}
public override async Task<object> Received(BasicDeliverEventArgs @event)
{
using var scope = _serviceProvider.CreateScope();
var permissionGrantCache = scope.ServiceProvider.GetService<IDistributedCache<PermissionGrantCacheItem>>();
var permissions = @event.ToDataObject<Dictionary<string, bool>>();
await permissionGrantCache.SetManyAsync(
permissions.Select(permission =>
new KeyValuePair<string, PermissionGrantCacheItem>(permission.Key, new PermissionGrantCacheItem(permission.Value))));
return Task.FromResult<object>(null);
}
}
}
Does this approach to refresh permissions in another app looks OK or I'm missing a better ABP approach?
UPDATE: i've used RabbitMq to notify Angular 2 about permission changes in Angular 1. I am now trying to modify Angular 2 permission cache via SetManyAsync
- it adds the values to the cache in Angular 2, but it still does not reflect the changes on page refresh...
I am running two Angular apps: https://localhost:4200 (Permissions are assigned and consumed here) and https://localhost:4201 (Permissions are only consumed here). Each of these two Angular apps have own HTTP API host (and own full-fledged ABP-based solution) and they share the same Identity Server.
To control full set of permission in app Angular 1, I have Nuget package of Application.Contracts
project from Solution 2 in Application.Contracts
project of Solution 1:
So far so good. I can see and change role permissions:
The problem is that the changes made in Angular 1 are not reflected in Angular 2 - I still can see or not see the protected pages after page reload or re-login:
I've tried to override and play with IDistributedCache<PermissionGrantCacheItem>
, but it does not work - most likely because both hosts have separate caches. But how to resolve the described issue then?
I am sorry, but I am now allowed to share the project. What I can do is just to show some informational screendumps, send logs and separate code files... Please let me know if I can measure some additional timings somewhere, etc. Now - just some thoughts: when I use Postman to send API requests - first TTFB is large, that's right, second and further - 10 and more times decreased. That would explain DI initialization. But in real situation, when I refresh the same page in Angular app - I am getting almost the same big timings. For instance, first run ~5-6sec per lookup API request. Next run ~1.5-3sec. Next run - about the same time... Next run - attention - ~4sec again!! Even retrieving refresh tokens at IdentityServer takes ages:
UPDATE: I've updated my controllers - removing all appservices instantiation at all. There is no DB access to retrieve data now anywhere, empty data is returned:
[HttpGet]
[Route("values/owners/{languageCode?}")]
public virtual async Task<PagedResultDto<LookupItemDto<int>>> GetOwnerAsync([FromRoute] string languageCode = null)
{
return new PagedResultDto<LookupItemDto<int>> { Items = new List<LookupItemDto<int>>(), TotalCount = 0 };
}
Also, made all API calls anonymous and without audit logging. And TTFB is still unsasisfactory. Any ideas?
DB resides on Azure cloud. Everything the rest - on localhost. Just in case: I won't be able to create local copy of DB if it is needed for a deeper testing. But as I already mentioned, sometimes there are unexplained lags even if I don't access DB.
I returned from vacation and keep on testing. I am not sure now it's a DB thing.
I run API GET
request from DB via appservice and TTFB was about 3-4s. Then I replaced the AbpController
code which accesses DB via appservice with already mentioned above List<int>
data:
[HttpGet]
[Route("values/module-types/{languageCode?}")]
public virtual async Task<List<int>> GetModuleTypeAsync([FromRoute] string languageCode = null)
{
return new List<int>
{
0, 1, 2, 3, 4, 5, 6, 7, 999, 10000
};
// AppService DB request is commented!!!
}
And TTFB still is 3s!
What do you suggest? Probably it has something to do with appservice DI instantiation in AbpControllers? I tried to run it in several mins when writing to you - and the time is about 50-60ms! Another API request - with appservice engaged, run straight after this, is 200-300ms...
So in other words - sometimes TTFS is more or less OK. But sometimes it is inacceptibly slow. I need to find a root cause. Emphasizing - everything is tested on localhost, VS 2020 / 2022.
I took a look at the log, trying to find information for the case when TTFB was about 3s. Please have a look at this:
2021-09-07 18:01:35.693 +03:00 [INF] Route matched with {area = "app", controller = "FixCodeValue", action = "GetModuleType", page = ""}. Executing controller action with signature System.Threading.Tasks.Task
1[Volo.Abp.Application.Dtos.PagedResultDto
1[AbxEps.CT.Core.Shared.LookupItemDto`1[System.Int32]]] GetModuleTypeAsync(System.String) on controller AbxEps.CentralTools.Controllers.FixCodeValues.FixCodeValueController (AbxEps.CentralTools.HttpApi). 2021-09-07 18:01:38.816 +03:00 [INF] Executing action method AbxEps.CentralTools.Controllers.FixCodeValues.FixCodeValueController.GetModuleTypeAsync (AbxEps.CentralTools.HttpApi) - Validation state: "Valid" 2021-09-07 18:01:38.996 +03:00 [INF] Executed action method AbxEps.CentralTools.Controllers.FixCodeValues.FixCodeValueController.GetModuleTypeAsync (AbxEps.CentralTools.HttpApi), returned result Microsoft.AspNetCore.Mvc.ObjectResult in 180.5069ms.
UPDATE: for better understanding, I'm attaching zipped log file, where logging level was set to "Debug". This time I tried to run my problematic Details page again. Could you please have a look at the log, probably it will give you some idea?
https://1drv.ms/u/s!AhWdpZddvifTtxGBMcuy35NOzw31?e=fSMRYs
On some next run, those times could be 2-3 times less (about 1.5-2sec), which is still pretty much...
UPDATE 2: cold run today, audit and authorization is turned off - debug log displays the gap between those 2 entries:
2021-09-08 11:31:11.624 +03:00 [DBG] Executing controller factory for controller AbxEps.CentralTools.Controllers.FreeCodeValues.FreeCodeValueController (AbxEps.CentralTools.HttpApi) 2021-09-08 11:31:13.063 +03:00 [DBG] Executed controller factory for controller AbxEps.CentralTools.Controllers.FreeCodeValues.FreeCodeValueController (AbxEps.CentralTools.HttpApi)
What is happening in-between? Again, next run of this request takes 255ms, so could look like a DB stuff (and now the data is cached)... But yesterday I saw big time without DB query involved.
_identityUserAppService.GetListAsync
makes 2 DB request. one for count one for the list. and I think this duration is OK. 1.44second is acceptable with a fully featured request.
Do you say it adds 300ms when you derive fromAbpController
? See the AbpController base class which uses Lazy properties.
probably i was not precise enough. what i wanted to say is that response time is approx 300ms when not using appservice - just forming test data in controller directly as shown in the code above. if i access real DB data from identityUserService (this exact ABP service is taken just in test purpose, because you know it well) - it is already 4sec which i suppose is too much, because usually it is said one request needs to take less than 1 sec. so what would you suggest?