Not resolved. Reopening.
UPDATE: please have a look at what's going on during logout at Identity Server, probably would ring some bell to you: https://drive.google.com/open?id=12SCQSi6Je4G9cCTwPoxwf4CzFujMz0Rn&usp=drive_fs
To me it looks like concurrent calls to Task<IdentityUser> FindAsync
(which is eventually cancelled as seen from the attached log) from:
using AbxEps.CentralTools.AbxUsers;
using AbxEps.CentralTools.Extensions;
using AbxEps.CentralTools.Jobs;
using AbxEps.CentralTools.Sessions;
using AbxEps.CentralTools.Tenants;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Volo.Abp.Identity;
using Volo.Abp.IdentityServer.AspNetIdentity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace AbxEps.CentralTools.IdentityServer.Profile
{
public class AbxProfileService : AbpProfileService
{
private readonly ITenantRepository _abxTenantRepository;
private readonly IAbxUserRepository _abxUserRepository;
private readonly ISessionRepository _abxSessionRepository;
private readonly IJobRepository _abxJobRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
public AbxProfileService
(
IdentityUserManager userManager,
IAbxUserRepository abxUserRepository,
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory,
ICurrentTenant currentTenant,
ITenantRepository abxTenantRepository,
IHttpContextAccessor httpContextAccessor,
ISessionRepository abxSessionRepository,
IJobRepository abxJobRepository
)
:base(userManager, claimsFactory, currentTenant)
{
_abxTenantRepository = abxTenantRepository;
_abxUserRepository = abxUserRepository;
_httpContextAccessor = httpContextAccessor;
_abxSessionRepository = abxSessionRepository;
_abxJobRepository = abxJobRepository;
}
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
using (CurrentTenant.Change(context.Subject.FindTenantId())) //is invoked a couple of times and probably is invoking Task<IdentityUser> FindAsync too below
{
await base.GetProfileDataAsync(context);
...
}
}
public override async Task IsActiveAsync(IsActiveContext context)
{
using (CurrentTenant.Change(context.Subject.FindTenantId())) //is invoked a couple of times and invoking Task<IdentityUser> FindAsync below
{
var abpUser = await UserManager.GetUserAsync(context.Subject);
var abxUser = abpUser == null ? null : await _abxUserRepository.GetAsync(abpUser.Id);
await base.IsActiveAsync(context);
...
}
}
}
}
and the middleware:
private async Task OnSessionEndRequestAsync(HttpContext httpContext, IdentityUserManager userManager)
{
try
{
var user = await userManager.GetUserAsync(httpContext.User); // Cancellation exception!
...
}
catch(Exception ex)
{
_logger.LogError(ex, "Session End handling error");
}
}
during logout...
I think all in all you are right about that. But there's another problem. So I put the debug point in intercept
call - I'm getting there ONCE or TWO TIMES (those are request to Identity Server) after I already pressed "Logout" button:
So I release the debug point and receive this bunch of API requests with error 401... I have created another ticket (https://support.abp.io/QA/Questions/5781/Logout-does-not-actually-logs-out-in-Angular-app) - that I actually cannot logout from the page other than Home page. The guy there answers he cannot do anything, because the problem is not reproduced on his end. So I don't know whether these two problems are related and what to do.
I understand. I suspect it could be a back-end issue with Identity Server, so that token revocation is not successful. Could you please tell me how to troubleshoot this? I may send our Identity Server log for the period of logging out, so maybe you would notice the root cause. Here are one (sometimes two) requests which triggers after I click the "Logout" button: Afterwards I'm getting API requests from my component which are not supposed to be run, because I'm logging out - so they produce error 401. And eventually I'm landed to Home page without logging out.
Saying honestly, I still hoped I would be able to use unsubscriber$
in the component base class to control everything. It's a pity there's no some observable i can watch to change it... I would prefer not to scatter the changes around different classes (e.g. HttpInterceptor
, etc.)
Well, at least it's possible make it working using AbpPermissionOptions
.. I don't know why it requires it.
But seems like I managed to make "dynamic" permission definition provider work properly combining "static" and "dynamic" values:
private IDictionary<string, PermissionGroupDefinition> _dynamicPermissionGroupDefinitions = new Dictionary<string, PermissionGroupDefinition>();
protected IDictionary<string, PermissionGroupDefinition> PermissionGroupDefinitions
{
get
{
var staticGroupDefinitions = _lazyPermissionGroupDefinitions.Value;
var combinedGroupDefinitions = new Dictionary<string, PermissionGroupDefinition>(staticGroupDefinitions);
foreach (var userAddedGroupDefinition in _dynamicPermissionGroupDefinitions)
{
combinedGroupDefinitions[userAddedGroupDefinition.Key] = userAddedGroupDefinition.Value;
}
return combinedGroupDefinitions;
}
}
private readonly Lazy<Dictionary<string, PermissionGroupDefinition>> _lazyPermissionGroupDefinitions;
private IDictionary<string, PermissionDefinition> _dynamicPermissionDefinitions = new Dictionary<string, PermissionDefinition>();
protected IDictionary<string, PermissionDefinition> PermissionDefinitions
{
get
{
var staticDefinitions = _lazyPermissionDefinitions.Value;
var combinedDefinitions = new Dictionary<string, PermissionDefinition>(staticDefinitions);
foreach (var userAddedDefinition in _dynamicPermissionDefinitions)
{
combinedDefinitions[userAddedDefinition.Key] = userAddedDefinition.Value;
}
return combinedDefinitions;
}
}
private readonly Lazy<Dictionary<string, PermissionDefinition>> _lazyPermissionDefinitions;
protected AbpPermissionOptions Options { get; }
private readonly IServiceProvider _serviceProvider;
public ExtendedStaticPermissionDefinitionStore(
IServiceProvider serviceProvider,
IOptions<AbpPermissionOptions> options)
{
_serviceProvider = serviceProvider;
Options = options.Value;
_lazyPermissionDefinitions = new Lazy<Dictionary<string, PermissionDefinition>>(
CreatePermissionDefinitions,
isThreadSafe: true
);
_lazyPermissionGroupDefinitions = new Lazy<Dictionary<string, PermissionGroupDefinition>>(
CreatePermissionGroupDefinitions,
isThreadSafe: true
);
}
public void AddPermissionDefinitionProvider(IPermissionDefinitionProvider permissionDefinitionProvider)
{
using (var scope = _serviceProvider.CreateScope())
{
var context = new PermissionDefinitionContext(scope.ServiceProvider);
permissionDefinitionProvider.PreDefine(context);
permissionDefinitionProvider.Define(context);
permissionDefinitionProvider.PostDefine(context);
foreach (var group in context.Groups)
{
_dynamicPermissionGroupDefinitions[group.Key] = group.Value;
}
var permissions = new Dictionary<string, PermissionDefinition>();
foreach (var groupDefinition in _dynamicPermissionGroupDefinitions.Values)
{
foreach (var permission in groupDefinition.Permissions)
{
AddPermissionToDictionaryRecursively(permissions, permission);
}
}
foreach (var permission in permissions)
{
_dynamicPermissionDefinitions[permission.Key] = permission.Value;
}
}
}
Probably would come in handy for someone. Closing the ticket... Thanks for inspiration!
Sure.
3 files here: Extension class which makes use of this "dynamic" provider,
overridden Static Store where the exception happens:
var providers = Options
.DefinitionProviders
.Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider)
.ToList()
if I don't use the Configure<AbpPermissionOptions>
approach
and the PortalAccessPermissionDefinitionProvider
itself.
Yes, I can and it works. However, I'd prefer to mark the class with attribute like DisableConventionalRegistration
and be sure it is ignored everywhere. With this approach:
Configure<AbpPermissionOptions>(options =>
{
options.DefinitionProviders.Remove(typeof(PortalAccessPermissionDefinitionProvider));
})
I need to remember doing it in every project which uses the given class...
This approach works - I've already checked it before. But the problem is that I have hundreds of components, each of which has dozen of API calls. The components use base class which contains unsubscriber$ and it is used in ngOnDestroy
to be nullified and complete. Thanks to that, when I go to another component, any API anywhere is properly unsubscribed (takeUntil(this.unsubscriber$)
).
Making use of one more check means I need to add this filter(...)
in all those API calls. And this is not a very good approach, IMHO.... I'd prefer to make changes in the base class unsubscriber$ only - to cover logout scenario...
I think I've almost managed to do this. However, quick question. How to prevent PermissionDefinitionProvider
from being automatically handled by ABP? I've decorated it with DisableConventionalRegistration
. But this is not enough, because Static Store uses one more mechanism, Options.DefinitionProviders
, to read them. So in addition to DisableConventionalRegistration
I need to manually remove it from IPermissionOptions
DefinitionProviders
collection (something for ABP to think about).
But probably there's a more comfortable way to make ABP ignore the provider?
The requested service 'AbxEps.CT.Core.Permissions.PortalAccessPermissionDefinitionProvider' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency. Autofac.Core.Registration.ComponentNotRegisteredException: The requested service 'AbxEps.CT.Core.Permissions.PortalAccessPermissionDefinitionProvider' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency. at Autofac.ResolutionExtensions.ResolveService(IComponentContext context, Service service, IEnumerable
1 parameters) at Autofac.ResolutionExtensions.Resolve(IComponentContext context, Type serviceType, IEnumerable
1 parameters) at Autofac.ResolutionExtensions.Resolve(IComponentContext context, Type serviceType) at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.GetRequiredService(Type serviceType) at AbxEps.CT.ModulePermission.ExtendedStaticPermissionDefinitionStore.<>c__DisplayClass17_0.<CreatePermissionGroupDefinitions>b__0(Type p) at System.Linq.Enumerable.SelectIListIterator2.ToList() at AbxEps.CT.ModulePermission.ExtendedStaticPermissionDefinitionStore.CreatePermissionGroupDefinitions() at System.Lazy
1.ViaFactory(LazyThreadSafetyMode mode) --- End of stack trace from previous location --- at System.Lazy`1.CreateValue() at AbxEps.CT.ModulePermission.ExtendedStaticPermissionDefinitionStore.get_PermissionGroupDefinitions() at AbxEps.CT.ModulePermission.ExtendedStaticPermissionDefinitionStore.GetGroupsAsync()