We used a built-in ABP cache in our ABP framework-based solutions plus Ocelot gateway project which aggregated the permissions from different sites plus RabbitMQ synchronization for updating the permissions cache on all the sites once these permissions are updated on the single Permission Management page.
We now decided to abandon this structure in favor of Redis server cache.
Still, it is not clear for me, how to get the list of ALL the permissions (for all the sites) having now Redis server cache at hand?
I took a look at the keys of the running Redis server for the started applications, but the naming is a mess and it does not look like it contains the permissions in principle... Maybe I need to add some code to manually place ALL the permissions in Redis cache after the application has started?? Which looks a bit weird...
This issue has been reported as a bug at GitHub, but ignored for many days, so I have to submit a commercial ticket to get a better attention.
2024-06-20 14:19:04.849 -05:00 INF () Lock is acquired for TokenCleanupBackgroundWorker
[14:19:04 INF] Lock is acquired for TokenCleanupBackgroundWorker
2024-06-20 14:19:04.859 -05:00 ERR () An exception was thrown while activating Volo.Abp.OpenIddict.Tokens.TokenCleanupService -> ?:OpenIddict.Abstractions.IOpenIddictTokenManager -> Volo.Abp.OpenIddict.Tokens.AbpTokenManager -> Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenCache -> Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenStore.Autofac.Core.DependencyResolutionException: An exception was thrown while activating Volo.Abp.OpenIddict.Tokens.TokenCleanupService -> ?:OpenIddict.Abstractions.IOpenIddictTokenManager -> Volo.Abp.OpenIddict.Tokens.AbpTokenManager -> Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenCache -> Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenStore.
---> Autofac.Core.DependencyResolutionException: None of the constructors found on type 'Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenStore' can be invoked with the available services and parameters:
Cannot resolve parameter 'Volo.Abp.OpenIddict.Tokens.IOpenIddictTokenRepository repository' of constructor 'Void .ctor(Volo.Abp.OpenIddict.Tokens.IOpenIddictTokenRepository, Volo.Abp.Uow.IUnitOfWorkManager, Volo.Abp.Guids.IGuidGenerator, Volo.Abp.OpenIddict.Applications.IOpenIddictApplicationRepository, Volo.Abp.OpenIddict.Authorizations.IOpenIddictAuthorizationRepository, Volo.Abp.OpenIddict.AbpOpenIddictIdentifierConverter, Volo.Abp.OpenIddict.IOpenIddictDbConcurrencyExceptionHandler)'.
We cannot provide the code of our solution. But the scenario we have been able to reproduce this is as follows (I guess there are other ones, this might just give you the idea where in the code to look at):
We created ABP project and added Volo.Docs module as described in the tutorial. The next step was configuring a document project for "FileSystem" and creating a file with the article.
We faced the following error when trying to go to this article:
ORA-12899 value too large for column "ABDEV"."DOCSDOCUMENT"."CONTENT" (actual: 2501, maximum: 2000).
Why is the file content stored in DB instead of file system? And how this limitation can be avoided? Just to anticipate some your questions: the project is commercial and we cannot share the source code.
I am observing a very weird behavior of permissions.
Let's say I have Tenant1... TenantX. I never have had any issues with accessing API resources protected with permissions by the users of these tenants. For simplicity, let's take the role "admin" - this is particularly where the issue is reproduced.
So, now I have received the complaint from TenantY. His users - which have the "admin" role assigned - cannot access specific resources (getting error 403) whereas - what is even more confusing - the other resources (and corresponding permissions) do not have such an issue.
"admin" role for TenantY does not differ from "admin" role in other tenants (at least, in UI).
I checked the DB and tenant settings thoroughly, but cannot see anything unusual.
Also, the data in AbpPermissionGrants looks the same for all tenants "admin" roles, i.e. all relevant tenants "admin" role have the complained permission assigned.
Another important note: now when I create a new tenant - I observe the same issue with error 403.
Do you have the idea what could be wrong, where should I check which settings?
I'm trying to insert about 400 entries to the table, the table is simple. The operation is very slow (maybe it's due to the communication between localhost where the app server is and Azure DB server, but I just want to find out before jumping to the conclusions).
Here is the code (chunkSize is 100):
private async Task TryCreateChunkAsync(List<GeoPoint> geoPoints, int chunkSize)
{
var dbContext = await _geoPointRepository.GetDbContextAsync();
dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
for (var skip = 0; skip < geoPoints.Count; skip += chunkSize)
{
using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
await _geoPointRepository.InsertManyAsync(geoPoints.Skip(skip).Take(chunkSize), autoSave: false);
await uow.CompleteAsync();
}
}
dbContext.ChangeTracker.AutoDetectChangesEnabled = true;
}
I don't know if this approach makes sense - I also tried a different approach, using DbContext, prior to that set BatchSize to 100 for our database:
var dbContext = await _geoPointRepository.GetDbContextAsync();
((MyDbContext)dbContext).AddRange(geoPoints);
await dbContext.SaveChangesAsync();
No difference. It is still very slow. There is no third-party tools for bulk operations for Oracle, so this is not an option in any case. Besides, according to some article online, the AddRange approach on 100 entries per DB round-trip is only 1.5 times slower than using bulk insert on such amount of data.
Another thing - I am not sure that ABP logging makes the insertion much slower, but I was unable to turn off these things:
despite applying [DisableAuditing] attribute to Controller or AppService method. Please note, that I don't want to use IgnoreUrls, since the Url might change in future - only the method body is relevant. I also don't want to apply [DisableAuditing] to the entity, because in some other methods I DO want the logging as usual.
What puzzles me is: >
Executed DbCommand (XXXms)
all the DbCommand take more or less appropriate amount of time (each DbCommand takes less than 1 second).
But look at this:
Executed action YYY.Controllers.GeoPoints.GeoPointController.UploadAsync (YYY.HttpApi) in 53197.6736ms
I can't explain this. I turned off the auditing by putting the attribute [DisableAuditing] on the entity class.The time for all the operations does not sum up. Why the total time is so huge, what most of this total time is spent for?? It is somehow related to the fact I'm inserting many records. Because when I do some elementary DB operation - the API request time is normal.
ABP Framework version: v7.0.1 UI Type: Angular Database System: EF Core Auth Server Separated
I observe strange behavior of ABP memory cache (DistributedCache). But I suspect it has something to do with the relevant services DI registrations.
On Application level, I register two DI instances as a singleton: a cache and a cache manager which reads or update this cache (and yes - I DO have this on Domain level - DependsOn[(...typeof(AbpCachingModule)...)]):
public class MyApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
...
context.Services.AddSingleton<IDistributedCache<CockpitCacheItem>, DistributedCache<CockpitCacheItem>>();
context.Services.AddSingleton<CockpitCacheManager>((IServiceProvider provider) => new CockpitCacheManager(provider));
...
}
}
So I have a single cache instance of this type per solution. It is identified by a single name - so there is only ONE cache ITEM per solution.
However, when I call the method cockpitCacheManager.UpdateAsync - which retrieves and updates the cache item - from HttpApi.Host project (class MyRabbitMqReceiver below), FOR THE FIRST TIME it ALWAYS reports that the cache item is not present in the cache (is null) - so it takes the data from DB. And beginning from the next invocation it's already OK (the item is in cache):
public class MyHttpApiHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
...
context.Services.AddSingleton((IServiceProvider provider) => new MyRabbitMqReceiver(provider, "myQueueName"));
...
}
}
public class MyRabbitMqReceiver: RabbitMqReceiverBase
{
...
private async Task HandleCacheItemAsync(...)
{
var cockpitCacheManager = _serviceProvider.GetService<CockpitCacheManager>();
await cockpitCacheManager.UpdateAsync(cacheFieldValue1, cacheFieldValue2); //IT DOES NOT SEE THE CACHE ITEM WHEN INVOKED FOR THE FIRST TIME!
...
}
}
I won't bore you with cockpitCacheManager.UpdateAsync implementation, because the part related to retrieving a cache item is standard (using your GetOrAddAsync method).
The item is 100% in the cache, I fill it from DB prior to the requesting.
Interesting fact is that if I request this cache item from Application layer - it's always retrieved as expected.
I have custom Permission Definition Provider:
using AbxEps.CT.Core.Extensions;
using AbxEps.CT.Core.Localization;
using AbxEps.CT.Core.PortalMenu;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
using Volo.Abp.Threading;
using Volo.Abp.Users;
namespace AbxEps.CT.Core.Permissions
{
public class CorePermissionDefinitionProvider : PermissionDefinitionProvider
{
private readonly ICurrentUser _currentUser;
private readonly IAbxPortalsMenuAppService _abxPortalsMenuAppService;
public CorePermissionDefinitionProvider
(
IAbxPortalsMenuAppService abxPortalsMenuAppService,
ICurrentUser currentUser
)
{
_abxPortalsMenuAppService = abxPortalsMenuAppService;
_currentUser = currentUser;
}
public override void Define(IPermissionDefinitionContext context)
{
var coreCtGroup = context.AddGroup(CorePermissions.GroupName, L("Permission:Core"));
var fileMngPermission = coreCtGroup.AddPermission(CorePermissions.FileManager.Read, L("Permission:FileManager:Read"));
fileMngPermission.AddChild(CorePermissions.FileManager.Modify, L("Permission:FileManager:Modify"));
if (_currentUser.IsAuthenticated)
{
AsyncHelper.RunSync(() => InitPortalAccessPermissionsAsync(context)); // Not called, because when the host is started the user is still not authenticated - so "if" condition is not invoked
}
}
private async Task InitPortalAccessPermissionsAsync(IPermissionDefinitionContext context)
{
...
}
}
}
Whereas the permissions which are supposed to be added unconditionally (fileMngPermissions) are added successfully when host is running, the permissions which need to be added only after the user is authenticated (portalAccessPermissions) are obviously not added and not visible via IPermissionAppService:
var allPermissionsForRole = await _permissionAppService.GetAsync("R", "Role 1");
How to add a whole CorePermissionDefinitionProvider or the part of its relevant permissions (portalAccessPermissions) conditionally - once the user got authenticated?
P.S. I'd prefer not to use Middleware, because its Invoke method is invoked on each request. Instead, I need to add my CorePermissionDefinitionProvider or its authentication-related permissions once after the user authentication was successful. I use external authentication via Identity Server. So this functionality needs to reside inside the application project, not Identity Server project.
Making PermissionDefinitionManager a ITransientDependency (or probably I can make CorePermissionDefinitionProvider transient too) does not sound good either: I dot not want to trigger the check each time. I just need to trigger it ONCE, but after the user has been authenticated.
Hi. I can't say for sure in which moment it has begun, but now if I try to logout from the page OTHER than Home page (root page) - the logout does not happen. Here is the workflow of what is happening when I'm clicking "Logout" button in my Angular app:
First connect/revocation request contains "access_token". Next "connect/revocation" request contains "refresh_token.
Eventually I am just redirected to Home page.
Also the whole process is very slow and not attractive visually: at first the navigation and top menu gets disappears, so I see only the current page instead of being instantly redirected to Identity Server Login box (where I do not get at all in my case).
I don't know where to look at: is it a problem of front-end or back-end. Please point me in right direction.
Additionaly details: we DO have "AuthGuard" in app-routing.module.ts for each page we are trying to logout from.
Marked in red is yet another issue which is still not resolved: the subscribed API calls in the current Component: seems like at some point the user IS getting logged out (and thus is not authorized), so these requests are not allowed (but later the user is kind of logged-in again automatically).
Sorry I cannot share the source code with you. So I will be glad to follow your recommendations in troubleshooting the logout scenario.
ABP 7.0.1 / Angular
My home page shows some information to an authenticated user via API calls. If the user logs out - these methods need not to be invoked anymore.
Seems like I've tried all possible ways - and it still DOES call those methods with "Not authorized (401)" error from server after I click "Logout" button. I also have tried to call Subscription$.unsubscribe() while logging out, but it still does not work.
Another question: I can logout from any page, not just Home page. There are plenty of API call subscriptions on each of them. How am I supposed to unsubscribe from all such calls with minimal code changes??
Here is the piece of the code of my Home page:
ngOnInit() {
this.oAuthService.events
.pipe(
filter(event => event?.type === 'logout'),
tap(() => {
this.logout$.next(null); //those are called, but API calls are still invoked
this.logout$.complete();
}))
.subscribe();
this.homeService.getNewsForHomePage()
.pipe(filter(() => this.configStateService.getDeep('currentUser.isAuthenticated')), takeUntil(this.destroy), takeUntil(this.logout$))
.subscribe((newsResponse) => {
...
});
this.homeService.getUrlsForHomePage()
.pipe(filter(() => this.configStateService.getDeep('currentUser.isAuthenticated')), takeUntil(this.destroy), takeUntil(this.logout$))
.subscribe((newsUrlParameterResponse) => {
...
});
}
ngOnDestroy(): void {
this.destroy.next(null);
this.destroy.complete();
}
Moreover - when I am already at this page (where this.configStateService.getDeep('currentUser.isAuthenticated') is supposed to be false, I guess):
the API calls are still invoked.
After switching the tenant I need to reload navigation menu according to the current tenant's user permissions. I cannot manage to do this: when I switch to the tenant who does not have the proper permissions - the menu items are hidden. However, when I switch back - the menu items which need to be visible are not shown:
switch(tenant: Models.Common.Lookup<string>) {
this.oAuthService.configure(this.environment.oAuthConfig);
let loadingToasterId = Number(this.toaster.info('::Tenants:PleaseWait', '::Tenants:Switching', { ...this.toasterOptions && { sticky: true } }));
return from(this.oAuthService.loadDiscoveryDocument())
.pipe
(
switchMap(() => from(this.oAuthService.fetchTokenUsingGrant('switch_tenant', { token: this.oAuthService.getAccessToken(), tenant: tenant.id }))),
take(1)
)
.subscribe({
complete: () => {
this.toaster.remove(loadingToasterId);
this.toaster.success('::Tenants:SwitchingSucceeded', '::Tenants:Switching', { ...this.toasterOptions && { life: 2000 } });
this.sessionStateService.setTenant({ id: tenant.id, name: tenant.displayName, isAvailable: true } as CurrentTenantDto);
this.configStateService.refreshAppState().subscribe(x => {
this.router.navigate(['/'], { skipLocationChange: false, onSameUrlNavigation: 'reload' }).then(ready => {
if (ready) {
//TODO: is it really needed? What to do here?
}
});
});
},
error: () => {
this.toaster.remove(loadingToasterId);
this.toaster.error('::Tenants:SwitchingFailed', '::Tenants:Switching', { ...this.toasterOptions && { life: 2000 } });
}
});
}
The method which is triggered after switching the tenant:
init() {
this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true);
this.routesService.refresh();
combineLatest([this.configStateService.getDeep$('extraProperties.modulePermissionMap'), this.routesService.flat$.pipe(take(1))])
.pipe
(
filter(([modulePermissionMap, route]) => Object.keys(modulePermissionMap).length > 0 && Object.keys(route).length > 0),
takeUntil(this.destroy)
)
.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.routesService.remove(permissionProhibitedPageIds);
this.routesService.refresh(); //tried this, but it does not help - nonLazyLoadedRoute seem to contain menu items, however they are not displayed as expected
});
}
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) {
if (!this.appSettingInitialized) {
this.abxTenantSwitcher.init(); // tenant switching functionality
this.modulePermissionHandler.init(); // custom permission handling - init method shown previously
this.appSettingInitialized = true;
}
}
});
}