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).

Related Articles:

This entry was posted in fubumvc. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://blog.scooletz.com/ Scooletz

    I do like the idea of applying an IDataRestrictions. The very similiar paradigm you can find in my authorization project called Themis: http://themis.codeplex.com/

    It allows you not only apply simple conditions based on entities, but also crosscut it with properties of user user roles (for instance filtering based on a security level of a document and a reader role).

  • http://larsroald.wordpress.com Lars-Erik Roald

    I am sorry, but I don’t like this. You are doing the ‘sensitivity filtering’ at the wrong abstraction level. It is not a concern of the data acces layer. It is very CRUD-ish. I would rather have domainService returning ISensitiveDto or IRestrictedDto. I also think that queries dont belong in the presentation layer – specifications that are entity-agnostic are better. The serviceA could retrieve a list of IServiceASpecifications.

  • http://www.lostechies.com/members/jflanagan/default.aspx Joshua Flanagan

    @Lars-Erik – IDataRestrictions are not part of the data access layer. They are part of the domain and can be leveraged by different parts of the application – including data access, as demonstrated here. In a follow up post I will demonstrate how these same data restrictions defined in the domain can be used to authorize access to URLs – nothing to do with data access.

  • Lars-Erik Roald

    I see. Looking forward to next post.

  • http://stackingcode.com/ Adam Boddington

    How do you handle restrictions on lazy loaded collections? Do you amend the collection with a query restriction in the property getter after loading, or do you have some way of getting the restriction into the SQL there too?

    Elegant solution by the way. I had something similar but abandoned it in favor of NHibernate filters because of the collection problem.