Sep 3 2011

Data Access in Fail Tracker–Row-Level Security with LINQ to NHibernate

Category: Fail Tracker | NHibernateMatt @ 03:17

This is the third and probably final post about how data access is performed in Fail Tracker.  I’ve previously shown you the basics of how its repository-pattern based approach and how a shared base SpecsFor test fixture is leveraged to simplify testing.  In this post, I’ll show you how the decorator pattern is employed to provide simple, pain-free row-level security, ensuring that users can only see projects and issues that they’ve been granted access to.

Why Security Matters

Fail Tracker is a multi-tenant application: a single instance my hold many projects for many different users.  Therefore, security is quite important.  I would consider it a major bug if the application disclosed project or story information to a user that shouldn’t have access to that information.  Essentially every operation in Fail Tracker has a security risk to it, and enforcing this cross-cutting concern in every action would be quite painful.  You would essentially have code that looks like this in every single action method:

public class DashboardController : AuthorizedFailTrackerController
{
    private readonly IRepository<Project> _projects;
    private readonly IRepository<User> _users;

    public DashboardController(IRepository<Project> projects, IRepository<User> users)
    {
        _projects = projects;
        _users = users;
    }

    public ActionResult Index()
    {
        //Get current user, identity has the user's E-mail address
        var currentUser = _users.Query().Single(u => u.EmailAddress == this.User.Identity.Name);

        var projects = (from p in _projects.Query()
                        //Only show projects that the user is a member of
                        where p.Members.Contains(currentUser)
                        select Mapper.Map<Project,ProjectDashboardViewModel>(p)).ToArray();

        return View(projects);
    }
}

While not overly complex, this is certainly not ideal.  Aside from muddying up the action methods themselves, it introduces additional testing requirements and makes the code far more brittle, and it greatly increases the risk of a security problem in the application.  What happens if a developer implementing a new feature forgets to limit the data coming back from a query by the current user?  A better solution is needed, one that is transparent at the action method level.  IMO, the ideal solution is one that is baked in to the application framework itself and that is automagically applied to all features implemented in the future.

Implementing Row-Level Security Using the Decorator Pattern

Security is one area where Fail Tracker’s use of the repository pattern really pays off.  It is downright trivial to add row-level security without actually changing the repository layer.  This is accomplished by using the decorator pattern:

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

The idea is pretty simple: by applying a security decorator to the repository via the IoC container (which, by the way, powers everything in Fail Tracker), we can control access to the repository without having to actually change the repository itself or how it’s utilized.  Recall that the the repository interface and its implementation in Fail Tracker is indeed quite simple:

public interface IRepository<TEntity>
{
    void Save(TEntity entity);

    IQueryable<TEntity> Query();
}

public class NHibernateRepository<TEntity> : IRepository<TEntity>
{
    private readonly ISession _session;

    public NHibernateRepository(ISession session)
    {
        _session = session;
    }

    public void Save(TEntity entity)
    {
        _session.Save(entity);
        _session.Flush();
    }

    public IQueryable<TEntity> Query()
    {
        return _session.Query<TEntity>();
    }
}

The repository implementation is generic; it’s used for both Project and Issue access.  Unfortunately the steps needed to secure these two entities are slightly different.  With a Project, you can simply check its members.  For an Issue, you must locate its Project, then check the Project’s members.  Because of this, security cannot be implemented as a generic decorator and must be specific to each type:

public class ProjectSecurityDecorator : IRepository<Project>
{
    private readonly IRepository<Project> _repository;
    private readonly CurrentUser _user;

    public ProjectSecurityDecorator(IRepository<Project> repository, CurrentUser user)
    {
        _repository = repository;
        _user = user;
    }

    public void Save(Project entity)
    {
        _repository.Save(entity);
    }

    public IQueryable<Project> Query()
    {
        //TODO: Eventually Fail Tracker admins should be able to see everything. 
        return from p in _repository.Query()
               where p.Members.Contains(_user.Instance)
               select p;
    }
}

public class IssueSecurityDecorator : IRepository<Issue>
{
    private readonly IRepository<Issue> _repository;
    private readonly CurrentUser _user;
    
    public IssueSecurityDecorator(IRepository<Issue> repository, CurrentUser user)
    {
        _repository = repository;
        _user = user;
    }

    public void Save(Issue entity)
    {
        _repository.Save(entity);
    }

    public IQueryable<Issue> Query()
    {
        //TODO: Eventually Fail Tracker admins should be able to see everything. 
        return from i in _repository.Query()
               where i.Project.Members.Contains(_user.Instance)
               select i;
    }
}

With a little bit of StructureMap registry magic, all requests for either the Project or Issue repository will return decorated instances of the repository, ensuring that all data access in Fail Tracker is performed according to our security rules:

public class SecurityRegistry : Registry
{
    public SecurityRegistry()
    {
        For<CurrentUser>().Add<CurrentUser>();

        For(typeof(IRepository<Project>))
            .EnrichWith((ctx, obj) => new ProjectSecurityDecorator((IRepository<Project>)obj, ctx.GetInstance<CurrentUser>()));

        For(typeof(IRepository<Issue>))
            .EnrichWith((ctx, obj) => new IssueSecurityDecorator((IRepository<Issue>)obj, ctx.GetInstance<CurrentUser>()));
    }
}

Note that nothing changes in any of our controllers.  They can simply continue to consume IRepository<Project> or IRepository<Issue> as they always have.  They are completely ignorant of the security concerns, which is actually a good thing. I now have row-level security everywhere in Fail Tracker without needing to think about it.  This greatly simplifies new feature development and gives me much more confidence in making changes.  Plus, since security is encapsulated in two decorators instead of strewn about the codebase, it will be far easier to change how security works in the future should the need arise. 

“What’s that CurrentUser thing??”

Ahh, I almost forgot about that.  CurrentUser is a lazily-evaluated way to get the actual User object corresponding to the User making the web request:

public class CurrentUser
{
    private readonly IRepository<User> _users;
    private readonly IIdentity _currentUser;

    public CurrentUser(IRepository<User> users, IIdentity currentUser)
    {
        _users = users;
        _currentUser = currentUser;
    }

    public User Instance
    {
        get { return _users.Query().Single(u => u.EmailAddress == _currentUser.Name); }
    }
}

Note that it finds the current user via IIdentity, which the Fail Tracker application framework makes available via the container.  See the MvcRegistry if you want to know more.

More Than One Way to Skin a Cat

There are other ways to implement this sort of security.  Ayende’s Rhino Security framework is one such solution.  I considered using Rhino Security but felt that it was overly complex for what I needed, and my solution based on the repository and decorator patterns seemed sufficient.  Still, my solution isn’t perfect.  It bugs me that I need two decorators and can’t enforce security generically with a single decorator.  If anyone has suggestions on how to get around that, please let me know!

Tags:

blog comments powered by Disqus