How we systemically apply filters to our data access


I wrote about limiting access to sensitive information in Dovetail Support Center on the company blog, but didn’t get too deep into the technical implementation. Since that feature development relied heavily on the capabilities of our open source web framework, FubuMVC (and the related FubuFastPack), I figured it would be worthwhile to document how it all works. This post will describe how we filter data retrieved from the database.

RestrictedQuery

We wanted an easy way to systemically apply rules to filter out data, without every bit of client code having to know about those rules. Consider the “sensitive cases” feature I discussed in my Dovetail post – we needed to make sure that a user without the “View Sensitive Cases” permission would never see a case marked as sensitive. However, there are a number of places throughout the application where lists of cases are displayed: custom case queries, full-text search results, “heads up” widgets like “Cases opened by this contact in the past 30 days”, etc. We do not want to embed the logic for sensitive cases in each of these call sites for a few reasons:

  • it would be repetitive
  • we would have to remember to include the logic in every future piece of code that queried for cases
  • if we wanted to add a new rule, we would have to modify all of the call sites once again

We added a RestrictedQuery<T>() method to FubuFastPack’s Repository which can add additional criteria to  database queries. It is similar to our Query<T>() method, which is a lightweight wrapper around NHibernate LINQ, but it alters the query based on data restrictions defined in the application. A data restriction is defined by implementing the <a href="https://github.com/DarthFubuMVC/fubumvc/blob/3344dee1bfb5ccfa14cee5092bb283484bbbcd20/src/FubuFastPack/Querying/IDataRestriction.cs" target="_blank">IDataRestriction<T></a> interface:

public interface IDataRestriction<T> : IDataRestriction where T : DomainEntity
{
    void Apply(IDataSourceFilter<T> filter);
}

For example, to make sure sensitive cases are only shown to users with the necessary permission, we wrote this data restriction:

public class SensitiveCaseDataRestriction : IDataRestriction<Case>
{
    public void Apply(IDataSourceFilter<Case> filter)
    {
        if (!PrincipalRoles.IsInRole(DomainActions.ViewSensitiveCases))
        {
            filter.WhereEqual(x => x.IsSensitive, false);
        }
    }
}

Example

Consider a customer support system where support cases are  placed into a triage queue so that someone can prioritize them. In the application, you want to display a list of all cases in triage. For simplicity, ‘I’ll print the list to the console:

private void showCasesInTriageQueue(IQueryable<Case> allCases, string heading)
{
    Console.WriteLine();
    Console.WriteLine("=== {0} ===", heading);

    var foundCases = allCases.Where(c => c.Queue.Name == "Triage").ToArray();

    Console.WriteLine(foundCases.Length + " cases found");
    foreach (var foundCase in foundCases)
    {
        Console.WriteLine(foundCase.Title);
    }
}

The key logic is in line 6 where the list of cases is narrowed down to just the cases in the triage queue. Notice that the method is not clouded with other concerns like filtering out sensitive cases, or any other data restriction that may apply. We can call this method by passing in an IQueryable<Case> from Query<T>() or RestrictedQuery<T>():

// demonstrate that the current user does not have rights to view sensitive cases
PrincipalRoles.IsInRole(DomainActions.ViewSensitiveCases).ShouldBeFalse();

showCasesInTriageQueue(repository.Query<Case>(), "Query");

showCasesInTriageQueue(repository.RestrictedQuery<Case>(), "Restricted Query");

We first assert that the current user does not have rights to view sensitive cases. We then perform a regular Query, followed by a RestrictedQuery. The only difference is that RestrictedQuery applies all of the data restrictions that have been registered with the repository (via dependency injection). The output confirms that the sensitive case (regarding someone’s paycheck) was filtered out by the restricted query:

=== Query ===
3 cases found
Alert! Air temperature sensor not detected
Paycheck shows wrong 401k amount
Extra vacation days request

=== Restricted Query ===
2 cases found
Alert! Air temperature sensor not detected
Extra vacation days request

What’s extra nice is that the filtering happens at the database. The IDataSourceFilter<T> used in RestrictedQuery is implemented as a wrapper around NHibernate’s Criteria API. A quick look at NHibernate Profiler shows the two calls to the database:

restricted_query_nhprof

Summary

Creating additional system-wide filters is as easy as creating a new implementation of IDataRestriction<T> and making sure it gets registered in our inversion of control tool (most likely through automatic scanning). And since data restrictions do not contain any data access code, they can also be used in other contexts, such as authorizing access to pages (a post for another day).

Validate a Facebook JavaScript SDK cookie with Ruby