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();
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 .
When normal user is logged , filter will work.