JonPSmith / AuthPermissions.AspNetCore

This library provides extra authorization and multi-tenant features to an ASP.NET Core application.

Home Page:https://www.thereformedprogrammer.net/finally-a-library-that-improves-role-authorization-in-asp-net-core/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Exception using SignInManager

muliswilliam opened this issue · comments

I am getting the following exception when attempting to login a user with email and password:

System.ArgumentException: Expression of type 'System.Threading.Tasks.Task`1[System.Collections.Generic.List`1[AuthPermissions.DataLayer.Classes.RoleToPermissions]]' cannot be used for return type 'System.Threading.Tasks.Task`1[System.Collections.Generic.IEnumerable`1[AuthPermissions.DataLayer.Classes.RoleToPermissions]]'
   at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection`1 parameters, String paramName)
   at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable`1 parameters)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)

The login code is as follows:

var result = await _signInManager.PasswordSignInAsync(request.Email, request.Password, false, false);
    
if (result.Succeeded)
{
  var user = await _userManager.FindByEmailAsync(request.Email);
  var token = await _tokenBuilder.GenerateTokenAndRefreshTokenAsync(user.Id);

  return Ok(new UserLoginResponse(token.Token, token.RefreshToken));
}
else
{
  return BadRequest(new {Message = "Email or password is incorrect"});
}

The exception is from a query of RoleToPermissions which must be within my code, but I can't see anything that matches that. I have also run the Example2 and it works. Make sure you have the latest version of the AuthP.

If it still throws an exception, then you need to find where it happens - if the exception doesn't provide that then you need to debug it to find where it goes wrong.

The issue seems to be in CalcPermissionsForUserAsync in ClaimsCalculator. The changes are:

  • Added .Include(x => x.Role) fetching permissions
  • Added
     .Include(x => x.UserTenant)
     .ThenInclude(x => x.TenantRoles)

when fetching tenant Roles.

When I made the changes, the code does not throw the exception.

The whole method is as follows:

private async Task<string> CalcPermissionsForUserAsync(string userId)
{
    var permissionsForAllRoles = (await _context.UserToRoles
        .Include(x => x.Role)
        .Where(x => x.UserId == userId)
        .Select(x => x.Role.PackedPermissionsInRole)
        .ToListAsync());

    if (_options.TenantType != TenantTypes.NotUsingTenants)
    {
      var tenantRoles = await _context.AuthUsers
        .Include(x => x.UserTenant)
        .ThenInclude(x => x.TenantRoles)
        .Where(x => x.UserId == userId && x.TenantId != null)
        .ToListAsync();
        
        
      //We need to add any RoleTypes.TenantAdminAdd for a tenant user
      var autoAddRoles = tenantRoles
        .Select(x => x.UserTenant.TenantRoles.Where(y => y.RoleType == RoleTypes.TenantAutoAdd))
        .SingleOrDefault();

        if (autoAddRoles != null)
            permissionsForAllRoles.AddRange(autoAddRoles.Select(x => x.PackedPermissionsInRole));
    }

    if (!permissionsForAllRoles.Any())
        return null;

    //thanks to https://stackoverflow.com/questions/5141863/how-to-get-distinct-characters
    var packedPermissionsForUser = new string(string.Concat(permissionsForAllRoles).Distinct().ToArray());

    return packedPermissionsForUser;
}

Hi @muliswilliam,

Good work at tracking that down. I have tests for this type of use of the ClaimsCalculator, but it passes.

Can you provide the configuration of your application and the the list of roles with their RoleType so that I can create a failing test before I change anything.

Hi @JonPSmith,

Here's my configuration in Program.cs, I am using .net6:


// Register AuthPermissions
builder.Services.RegisterAuthPermissions<TamAuthPermissions>(options =>
  {
    options.TenantType = TenantTypes.SingleLevel;
    options.AppConnectionString = connectionString;
    // options.PathToFolderToLock = builder.Environment.WebRootPath;
    
    // This tells AuthP that you don't have multiple instances of your app running,
    // so it can run the startup services without a global lock
    options.UseLocksToUpdateGlobalResources = false;

    // This sets up the JWT Token. The config is suitable for using the Refresh Token with your JWT Token
    options.ConfigureAuthPJwtToken = new AuthPJwtConfiguration
    {
      Issuer = jwtBearerTokenSettings.Issuer,
      Audience = jwtBearerTokenSettings.Audience,
      SigningKey = jwtBearerTokenSettings.SigningKey,
      TokenExpires = new TimeSpan(0,5,0), // Quick Token expiration because we use a refresh token
      RefreshTokenExpires = new TimeSpan(1,0,0,0) // Refresh token is valid for one day
    };
  })
  .UsingEfCoreSqlServer(connectionString) //NOTE: This uses the same database as the individual accounts DB
  .IndividualAccountsAuthentication()
  .AddSuperUserToIndividualAccounts()
  .RegisterFindUserInfoService<IndividualAccountUserLookup>()
  .AddTenantsIfEmpty(AppAuthSetupData.TenantDefinition)
  .AddRolesPermissionsIfEmpty(AppAuthSetupData.RolesDefinition)
  .AddAuthUsersIfEmpty(AppAuthSetupData.UsersRolesDefinition)
  .SetupAspNetCoreAndDatabase(options =>
  {
    //Migrate individual account database
    options.RegisterServiceToRunInJob<StartupServiceMigrateAnyDbContext<AppIdentityDbContext>>();
    //Add demo users to the database
    options.RegisterServiceToRunInJob<StartupServicesIndividualAccountsAddDemoUsers>();
  });

The permissions are as below:

public enum TamAuthPermissions : ushort
{
  NotSet = 0, // Error Condition

  // Users Permissions
  [Display(GroupName = "Users", Name = "Read Users", Description = "This allows a user to read user's data")]
  UsersRead = 10,
  [Display(GroupName = "Users", Name = "Create Users", Description = "This allows a user to create users")]
  UsersCreate = 11,
  [Display(GroupName = "Users", Name = "Edit Users", Description = "This allows a user to edit users")]
  UsersEdit = 12,
  [Display(GroupName = "Users", Name = "Delete Users", Description = "This allows a user to delete users")]
  UsersDelete = 13,
  
  [Display(GroupName = "SuperAdmin", Name = "Access All Data", Description = "This allows user to access every feature and data", AutoGenerateFilter = true)]
  AccessAll = ushort.MaxValue,
}

The AppAuthSetupData which contains sample data is as follows:

public static class AppAuthSetupData
{
  public static readonly List<BulkLoadRolesDto> RolesDefinition = new List<BulkLoadRolesDto>()
  {
    new("Admin", null, "UsersCreate, UsersEdit, UsersRead, UsersDelete"),
    new("SuperRole", null, "AccessAll"),
  };
  
  public static readonly List<BulkLoadTenantDto> TenantDefinition = new()
  {
    new("Diageo"),
    new("Pets Ltd."),
    new("Big Rocks Inc.")
  };

  public static readonly List<BulkLoadUserWithRolesTenant> UsersRolesDefinition = new List<BulkLoadUserWithRolesTenant>
  {
    new ("User1@diageo.com", null, "Admin", tenantNameForDataKey: "Diageo"),
    new ("User2@diageo.com", null, "Admin", tenantNameForDataKey: "Diageo"),
    new ("User3@diageo.com", null, "Admin", tenantNameForDataKey: "Diageo"),
    new ("SuperAdmin1@diageo.com", null, "SuperRole"),

  };
}

Hi @muliswilliam,

OK, your setup is very standard and you don't use any tenant-specific Roles. My tests include some to check this and I can't see why it doesn't work. So, could you please

  1. Update your clone of the AuthP repo (I found tests that were out of data and updated them) and run all the tests and tell me if they all pass. I think they will pass, but this just that there isn't different on your PC.

  2. Could you update the CalcPermissionsForUserAsync method you updated the the code below. This is a bit quicker than your suggestion as it loads less data. If this works I will release a new version with this change.

private async Task<string> CalcPermissionsForUserAsync(string userId)
{
    //This gets all the permissions, with a distinct to remove duplicates
    var permissionsForAllRoles = await _context.UserToRoles
        .Where(x => x.UserId == userId)
        .Select(x => x.Role.PackedPermissionsInRole)
        .ToListAsync();

    if (_options.TenantType != TenantTypes.NotUsingTenants)
    {
        //We need to add any RoleTypes.TenantAdminAdd for a tenant user

        var autoAddRoles = await _context.AuthUsers
            .Where(x => x.UserId == userId && x.TenantId != null)
            .SelectMany(x => x.UserTenant.TenantRoles.Where(y => y.RoleType == RoleTypes.TenantAutoAdd))
            .ToListAsync();

        if (autoAddRoles.Any())
            permissionsForAllRoles.AddRange(autoAddRoles.Select(x => x.PackedPermissionsInRole));
    }

    if (!permissionsForAllRoles.Any())
        return null;

    //thanks to https://stackoverflow.com/questions/5141863/how-to-get-distinct-characters
    var packedPermissionsForUser = new string(string.Concat(permissionsForAllRoles).Distinct().ToArray());

    return packedPermissionsForUser;
}

It's frustrating that I can't create a test that fails, but in the end the changed query is just as fast and a bit easier to understand.

Let me know your result with the changed CalcPermissionsForUserAsync code.

@JonPSmith The above code for CalcPermissionsForUserAsync works. You can release a new version with the code. I will run the tests and update this comment.

Version 2.3.1 fixes this issue. Thanks for your patience @muliswilliam