Jasinski Technical Wiki

Navigation

Home Page
Index
All Pages

Quick Search
»
Advanced Search »

Contributor Links

Create a new Page
Administration
File Management
Login/Logout
Your Profile

Other Wiki Sections

Software

PoweredBy

Data-Level Security - ASP.NET Core and EF Core

RSS
Modified on Fri, Aug 27, 2021, 10:26 AM by Administrator Categorized as ASP·NET MVC, ASP·NET Security, EF Core

Overview

TODO

Reusable Code

Notes

  • _context and _db is a DatabaseContext
  • AuthMeta and AuthorizedMeta.Current refer to an object which contains info about the currently logged-in user.
  • UserTypeId is a general categorization of users, which may or may not be used in a particular system.

IUserCanAccess Interface

public interface IUserCanAccess
{
    bool UserCanAccessEntity(NetSoftDataModel db, int id, string userId, int? labId, UserTypeEnum userTypeId);
}

SecurityHelper Static Class

using Microsoft.AspNetCore.Http;
using System;

public static class SecurityHelper
{
    public  static int? GetLabId(HttpRequest request, bool allowNull = false)
    {
        /*--- If the user is assigned a lab, return that lab ---*/
        if (AuthorizedMeta.Current?.LabId != null)
        {
            return AuthorizedMeta.Current.LabId;
        }

        /*--- Otherwise, check the query string ---*/
        var labId = GetQueryStringItem(request, "labId");

        if (labId == null && !allowNull)
        {
            throw new Exception("Invalid Lab ID");
        }

        return labId;
    }
    private static int? GetQueryStringItem(HttpRequest request, string key)
    {
        var val = request.Query.ContainsKey(key) ?
            request.Query[key][0] :
            null;

        if (int.TryParse(val, out int result))
        {
            return result;
        }

        return null;
    }
}

ValidateUserCanAccessAttribute

using System;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ValidateUserCanAccessAttribute : Attribute
{
    public string IdRouteParameterName { get; private set; }
    public Type EntityType { get; private set; }
    public ValidateUserCanAccessAttribute(Type entityType, string idRouteParameterName)
    {
        EntityType = entityType;
        IdRouteParameterName = idRouteParameterName;
    }
}

ValidateUserCanAccess Controller Extension Method

protected void ValidateUserCanAccess<T>(int id)
    where T : IUserCanAccess, new()
{
    var a = new T();

    /* TODO: Replace "Tenant" with the name of your tenant table.    */
    /* If not a multi-tenant application, replace with string.Empty. */
    var labId = a.GetType().Name == "Tenant" ? 
        AuthorizedMeta.Current.LabId : 
        SecurityHelper.GetLabId(Request);

    if (!a.UserCanAccessEntity(_context, id, AuthMeta.UserId, labId, AuthorizedMeta.Current.UserTypeId))
    {
        throw new UnauthorizedAccessException($"User can't access {a.GetType().Name} {id}.");
    }
}

GetRepoItems Method

This method can be added to the DataModel class to enforce data-level security on an entity throughout the application. NOTE: This assumes the DbSet for the entity cannot be accessed from outside the data layer - that is, it's declared "internal" or "private".

public IQueryable<T> GetRepoItems<T>()
    where T: class, new()
{
    var modelType = this.GetType();

    var totFullName = (typeof(T)).FullName;

    var prop = modelType.GetProperties()
        .FirstOrDefault(p =>
            p.PropertyType.IsGenericType
            && p.PropertyType.Name == "DbSet`1"
            && p.PropertyType.GenericTypeArguments[0].FullName == totFullName
            );

    var dbSet = prop.GetValue(this) as DbSet<T>;

    var q = dbSet?.AsQueryable<T>();

    if (q != null && typeof(IWhereUserCanAccess<T>).IsAssignableFrom(typeof(T)))
    {
        var userId = AuthorizedMeta.Current.UserId;

        // Note: AuthorizedMeta.Current.SelectedTenantId gets set in the global authorization
        // filter code based on a query string parameter.
        var tenantId = AuthorizedMeta.Current.SelectedTenantId;

        var userTypeId = AuthorizedMeta.Current.UserTypeId;

        q = q.Where((new T() as IWhereUserCanAccess<T>).WhereUserCanAccess(userId, tenantId, userTypeId));
    }

    return q;
}

IWhereUserCanAccess Interface

public interface IWhereUserCanAccess<T>
{
    Expression<Func<T, bool>> WhereUserCanAccess(string userId, int? selectedTenantId,
        UserTypeEnum userTypeId);
}

Usage

(1) Each data entity we want to enforce data-level security on should implement the IWhereUserCanAccess interface. Here's an example.

public class Item 
{
    public Expression<Func<Item, bool>> WhereUserCanAccess(string userId, int? selectedTenantId,
        UserTypeEnum userTypeId)
    {
        return a => !a.IsDeleted && a.TenantId == selectedTenantId;
    }
}

(2) The DbSet needs to be declared as "internal" instead of "public". This prevents code from outside the data later from bypassing the data-level security.

(3) Example service code

using (var db = new MyDataModel())
{
    var dbItem = db.GetRepoItems<Item>().FirstOrDefault(a => a.Id == id);
}

ScrewTurn Wiki version 3.0.1.400. Some of the icons created by FamFamFam. Except where noted, all contents Copyright © 1999-2024, Patrick Jasinski.