Search code examples
c#asp.net-coreasp.net-authorization

Ignoring global HasQueryFilter() for an administrator in a ASP.NET Core 8


I need to allow a 'SuperUser' overall access to data and views across multiple tenants.

I use a single database and a single codebase for the app. To isolate the data for each tenant a filter has been created in the modelbuilder in ApplicationDbContext.

However, the superuser should be able to bypass this filter and have access to all data (let's not worry, if that is a good thing or not). I know I can accomplish this by adding the IgnoreQueryFilters(), but then I need to add this to each and every filter.

Is there a way to accomplish this globally?

Your guidance is very much appreciated.

Edit

I have added the complete and revised ApplicationDbContext and Program.cs. Why does the if-statement around the codeblock HasQueryFilter() not work?

As was stated by Panagiotis Kanavos, the code should not call HasQueryFilter if the User is an Administrator. But how to accomlplish this? I tried below code, but that did not work.

ApplicationDbContext

using Whatever.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Whatever.Data
{
    public partial class ApplicationDbContext : 
IdentityDbContext<ApplicationUser, ApplicationRole, string,
    IdentityUserClaim<string>, ApplicationUserRole, 
IdentityUserLogin<string>, IdentityRoleClaim<string>, 
IdentityUserToken<string>>
{
    private readonly IHttpContextAccessor _contextAccessor;
    public 
ApplicationDbContext(DbContextOptions<ApplicationDbContext> 
options, IHttpContextAccessor httpContextAccessor)
        : base(options)
    {
        _contextAccessor = httpContextAccessor;
    }
   
    public virtual DbSet<ApplicationUser> ApplicationUser { get; set; }
    public virtual DbSet<ApplicationRole> ApplicationRole { get; set; }
    public virtual DbSet<ApplicationUserRole> ApplicationUserRole { get; set; }
    public virtual DbSet<Tenant> Tenant { get; set; }
    public virtual DbSet<TenantPerson> TenantPerson { get; set; }
    public virtual DbSet<Customer> Customer { get; set; }
    public virtual DbSet<CustomerMember> CustomerMember { get; set; }              

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);           

        modelBuilder.Entity<ApplicationUserRole>(userRole =>
        {
            userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

            userRole.HasOne(ur => ur.Role)
            .WithMany(r => r.UserRoles)
            .HasForeignKey(ur => ur.RoleId)
            .IsRequired();

            userRole.HasOne(ur => ur.User)
            .WithMany(r => r.UserRoles)
            .HasForeignKey(ur => ur.UserId)
            .IsRequired();
        });      
          
        if(!_contextAccessor.HttpContext.User.IsInRole("SuperUser"))  // Does not work properly
        {       
            modelBuilder.Entity<Tenant>().HasQueryFilter(e => e.Id == CurrentTenantId);
            modelBuilder.Entity<TenantPerson>().HasQueryFilter(e => e.TenantId == CurrentTenantId);
            modelBuilder.Entity<Customer>().HasQueryFilter(e => e.TenantId == CurrentTenantId);
            modelBuilder.Entity<CustomerMember>().HasQueryFilter(e => e.TenantId == CurrentTenantId);
        }          

        modelBuilder.Entity<Project>()
            .HasOne(e => e.Profile)
        .WithOne(e => e.Project)
            .HasForeignKey<Profile>(e => e.ProjectId)
            .IsRequired(false);
    }

    public virtual Guid CurrentTenantId => GetCurrentTenantId();

  private Guid GetCurrentTenantId()
  {
     string cUsername = _contextAccessor?.HttpContext?.User.Identity?.Name ?? string.Empty;
     ApplicationUser? storedUser = Users.FirstOrDefault(u => u.UserName == cUsername);
     return storedUser?.TenantId ?? Guid.Empty;
  }
  }
  }
  }
  }

Program.cs

using Whatever.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = 
builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddIdentity<ApplicationUser, ApplicationRole> 
   (options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultUI()
    .AddDefaultTokenProviders();

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");        
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Customer}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

Solution

  • In my test , your logic of retrieving the currentName is not finished correctly in modelCreating . The HasQueryFilter method needs to reference a property that is evaluated when the query is executed, not when the model is being built . Here is a code sample .

    ApplicationDbContext.cs

    public class ApplicationDbContext : IdentityDbContext
    {
    
        private readonly IHttpContextAccessor _contextAccessor;
    
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor contextAccessor)
        : base(options)
        {
            _contextAccessor = contextAccessor;
        }
    
        public DbSet<TenantPerson> TenantPersons { get; set; }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<ApplicationUser> Users { get; set; }
    
        public virtual Guid CurrentTenantId => GetCurrentTenantId();
    
        public bool ShouldApplyTenantFilter()
        {
            // Replace the logic below with your actual superuser check
            var isSuperUser = _contextAccessor.HttpContext.User.IsInRole("Super");
            return !isSuperUser;
        }
    
        private Guid GetCurrentTenantId()
        {
            string cUserName = _contextAccessor.HttpContext.User.Identity?.Name ?? string.Empty;
            ApplicationUser sUser = Users.FirstOrDefault(u => u.UserName == cUserName);
            return sUser?.TenantId ?? Guid.Empty;
        }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            //Logic of verifying if run into the filter
            modelBuilder.Entity<TenantPerson>().HasQueryFilter(e => ShouldApplyTenantFilter() ? e.TenantId == CurrentTenantId : true);
            modelBuilder.Entity<Customer>().HasQueryFilter(e => ShouldApplyTenantFilter() ? e.TenantId == CurrentTenantId : true);
        }
    }
    

    When SuperUser is logged , it will bypass the filter, get all the records .

    enter image description here

    When normal user is logged , filter will work.

    enter image description here enter image description here