Row Level Authorization with Entity Framework

A few posts back I wrote about the benefits of having a well encapsulated data access implementation. One of the benefits outlined there was the advantage it may bring when we need to implement row-level authorization at the data access level. In this post, I will detail this implementation on top of the iQuarc Data Access library.

By row-level authorization, we mean that we want to restrict the access to the rows of one or more entities based on the rights or the role of the current user. For example we want to ensure that users can only access those data rows that belong to their department, or we want to restrict a manager to see only the data rows related to the projects she manages.

To implement this at the data access level, it means that we should build a filter based on the current user roles (or claims) and apply it to each query that is issued on the entities for which rows we should restrict the access. If we have a well encapsulated data access it means that we can use it as the central place through which all the queries go and we could extend it to intercept and append the filter to the queries. This assures us a consistent implementation for row-level authorization. Otherwise, we would need to go through all the controllers or services that send queries and append the filter "by hand", which is error prone.

Lets take a more specific example. Say we have the following entities:


, and we want to restrict the access to the Order rows such that the current user, if she is an account manager, to see only the orders of the customers that are in her area. For this case, we need to append the order.Customer.AreaID == currentUserAreaID filter to all the queries that go on the Order entity.

To get this done in a consistent manner and for all the entities where we want to have row-level authorization, we need to have a generic approach. Here, a well encapsulated data access implementation, which is Linq based (like iQuarc.DataAccess is) plays an important role. We can build a Lambda Expression for the authorization filter and add a .Where() with it on the IQueryable<T> which we'll pass to the caller. Having it implemented on top of Entity Framework (EF), we can add the .Where() on to the DbContext.DbSet<T> property, like this:

public class Repository : IRepository
{
     public IQueryable<T> GetEntities<T>() where T : class
     {
         int currentUserValue = GetFilterValueFromCurrentUser();
         Expression<Func<T, bool>> authFilter = BuildWhereExpression<T>(currentUserValue);

         return Context.Set<T>()
                  .Where(authFilter)
                  .AsNoTracking();
     }
...
}

This is very much similar with what we did in the previous post, where we built a filter for filtering tenant specific data based on the .TenanatID property. The difference is that here, we cannot make all the entities for which we need row-level authorization to implement an interface like ITenantEntity was, and to have a TenantID property to use in the filter. This is a more generic case than the multitenacy example from the previous post. Here for the Order we need order.Customer.AreadID, but for the Customer we need customer.AreadID, and maybe for other entities we want to filter based on something else like OrganizationID or CountryCode.

Therefore, the BuildWhereExpression<T>() function is more complicated here. For each entity it would need to know the two operands from the filter expression:

For each entity we can specify these as row-level authorization policies and register them in a container that will be used from the repository. Such a policy should have a Lambda Expression which can select, starting from the current entity type, the property to be used in the authorization filter, and a function that can get the filer value from the current user. In a simplified form, the class that represent such a policy may look like this:

class RowAuthPolicy<TEntity, TProperty>
{
    public RowAuthPolicy(Expression<Func<TEntity, TProperty>> selector, IRowAuthPoliciesContainer parent)
    {
        Selector = selector;
        FilterValueGetter = () => default(TProperty);
        EntityType = typeof(TEntity);
    }

    public Expression<Func<TEntity, TProperty>> Selector { get; private set; }
    public Func<TProperty> FilterValueGetter { get; private set; }
    public Type EntityType { get; private set; }
}

The class is generic by the type of the entity to which it applies and by the type of the property that will be used in the filter. The property type and the filter value type must match.

Now, we can create a RowAuthPoliciesContainer with a fluent API that allows a nice and easy way to specify these policies for the entities we want row-level authorization. We'd like something like this:

public static IRowAuthPoliciesContainer ConfigureRowAuthPolicies()
{
    return new RowAuthPoliciesContainer()
        .Register<Order, int>(o => o.Customer.AreadID).Match(CurrentUserAreaId)
        .Register<Customer, int>(c => c.AreadID).Match(CurrentUserAreaId)
        .Register<SalesArea, string>(sa => sa.CountryCode).Match(CurrentUserCountryCode);
}

The Register() function receives the Lambda Expression that will be used to select the property to filter by, and then by calling the Match() function we can pass the function which will get the filter value from the current user. The CurrentUserAreaId() is just a static helper function. It is quite simple, something like this:

private static int CurrentUserAreaId()
{
    const string areaKeyClaim = "area_key";
    Claim areaClaim = ClaimsPrincipal.Current.FindFirst(areaKeyClaim);
    int areaId = ClaimsValuesCache.GetArea(areaClaim.Value);
    return areaId;
}

we may have more such helper functions, which we may use in more policies.

The RowAuthPoliciesContainer class will create the policy classes when Register() is called and will store them in a dictionary:

class RowAuthPoliciesContainer : IRowAuthPoliciesContainer

   readonly Dictionary<Type, object> policies = new Dictionary<Type, object>();

   public RowAuthPolicy<TEntity, TProperty> Register<TEntity, TProperty>(Expression<Func<TEntity, TProperty>> selector)
   {
       var policy = new RowAuthPolicy<TEntity, TProperty>(selector, this);
       policies.Add(policy.EntityType, policy);
       return policy;
   }

   public IRowAuthPolicy<TEntity> GetPolicy<TEntity>()
   {
       return (IRowAuthPolicy<TEntity>) policies[typeof(TEntity)];
   }

   public bool HasPolicy<TEntity>()
   {
       return policies.ContainsKey(typeof(TEntity));
   }
...
}

Now, lets go back to the Repository and its BuildWhereExpression<T>() function. One first thing to notice is that the filter value is now part of the row-level authentication policy, so it shouldn't be passed through the currentUserValue parameter to the function as we've written the code in the beginning. It may be of different types, not only int, but also string or others, like we have in the CountryCode policy in the above example. Moreover, the Repository.BuildWhereExpression<T>() can only be generic by the type of the entity, which means that it does not know the type of the property, and therefore it wouldn't be able to build the Lambda Expression. To fix all these we can delegate the building of the Lambda Expression to the policy class. So by refactoring it, we'll have:

public class Repository : IRepository
{
    private IRowAuthPoliciesContainer container;

    public IQueryable<T> GetEntities<T>() where T : class
    {
         Expression<Func<T, bool>> authFilter = BuildWhereExpression<T>(currentUserValue);
         return Context.Set<T>()
                  .Where(authFilter)
                  .AsNoTracking();
    }

    private Expression<Func<T, bool>> BuildWhereExpression<T>()
    {
        if (container.HasPolicy<T>())
        {
            IRowAuthPolicy<T> policy = container.GetPolicy<T>();
            return policy.BuildAuthFilterExpression();
        }
        else
        {
            Expression<Func<T, bool>> trueExpression = entity => true;
            return trueExpression;
        }
    }
...
}

The RowAuthPolicy<TEntity, TProperty> class also gets refactored to hide the Selector and the FilterValueGetter and to use them internally to build the Lambda Expression for the authentication filter:

class RowAuthPolicy<TEntity, TProperty> : IRowAuthPolicy<TEntity>
{
    private readonly Expression<Func<TEntity, TProperty>> selector;
    private Func<TProperty> filterValueGetter;
    private readonly IRowAuthPoliciesContainer parent;
    
    public RowAuthPolicy(Expression<Func<TEntity, TProperty>> selector, IRowAuthPoliciesContainer parent)
    {
        this.selector = selector;
        this.parent = parent;
        this.filterValueGetter = () => default(TProperty);
        EntityType = typeof(TEntity);
    }

    public Type EntityType { get; private set; }

    public Expression<Func<TEntity, bool>> BuildAuthFilterExpression()
    {
        TProperty value = filterValueGetter.Invoke();
        Expression<Func<TProperty>> filterValueParam = () => value;

        var filterExpression = Expression.Lambda<Func<TEntity, bool>>(
            Expression.MakeBinary(ExpressionType.Equal,
                Expression.Convert(selector.Body, typeof(TProperty)),
                filterValueParam.Body),
            selector.Parameters);

        return filterExpression;
    }

    public IRowAuthPoliciesContainer Match(Func<TProperty> filterValueGetFunc)
    {
        this.filterValueGetter = filterValueGetFunc;
        return parent;
    }
...
}

And with this we have completed the implementation. We now have row-level authentication consistently implemented for any entity we need. We added it only by extending the data access, so we could add it with a minimum effort even at a later stage of the project if we have a well encapsulated data access implementation, which assures that all the queries and commands go in a consistent manner through it. Now, with the policies we have defined, all the screens that show Customers, Orders or SalesAreas will automatically be filtered based on the access rights of the current user. If later we want that all the screens that show Products to also be filtered we just add a new registration in the policy container:

    .Register<Product, int>(p => p.Producer.AreaID).Match(CurrentUserAreaId)

You can find the entire source code of this sample in my Code Design Training GitHub repository here.


This can be further extended:

  • You may have more policies for one entity and use them based on some context or combine them in different ways. You can also add a .When() function to the RowAuthPolicy class to specify that the rule should only apply on a condition. For example the below registration says that the rule should only apply if the user is a sales person:
  .Register<SalesArea, string>(sa => sa.CountryCode).When(CurrentUserIsSales).Match(CurrentUserCountryCode);

  • Another thing to consider is if you have use cases when the related entities collections should also get filtered by this mechanism. This sample implementation only takes care of filtering the root queries. If you eager load the related entities some more work is needed. For example if you have the scenario with OrderLines which have Products that belong to an area which makes them not accessible by the current user, a rule like:
   .Register<OrderLine, int>(ol => ol.Product.Producer.AreaID).Match(CurrentUserAreaId)

will get applied when the OrderLines are loaded like:

var q = repository.GetEntities<OrderLine>()
     .Where(ol => ol.OrderID == orderId);
 return q.ToArray();

However, it will not be applied when they are loaded through the related entity collection like:

var c = repository.GetEntities<Order>()
        .Include(o => o.OrderLines)
    .Where(o => o.ID == orderId);

or like:

var q = repository.GetEntities<Order>()
    .Select(o => new
    {
        o.ID,
        o.OrderLines,
        o.OrderDate
    })
    .Where(o => o.ID == orderId);

For both these examples you need to parse the Lambda Expression and to rewrite it to append the authorization filter.

The .Include() is specific to EF, so you will need to make an EF specific parser. EF does not provide a way to filter the rows loaded with Include(), so you could use the EntityFramework-Plus 3rd party project that provides an .IncludeFilter() operator.

For the second example, you would need to rewrite the query into something like:

var q = repository.GetEntities<Order>()
    .Select(o => new
    {
        o.ID,
        OrderLines = o.OrderLines.Where(ol=>ol.Product.Producer.AreaID == areaId),
        o.OrderDate
    })
    .Where(o => o.ID == orderId);

This will filter the related entity collection when it is loaded and projected into the anonymous type.

More discussions on data access security are part of my Code Design Training
Featured image credit: maxkabakov via 123RF Stock Photo