Trifork Blog

Axon Framework, DDD, Microservices

Declarative multi-tenant security with Spring Security and Spring-MVC

September 5th, 2013 by
|

It’s been a while since our last ‘from the trenches’ entry, and as I’ve found I am better at authoring blogs than convincing colleagues to do the same I figured I’d write you another installment. This time I’d like to focus on an easy yet powerful approach that we used to secure a multi-tenant Spring-MVC application using Spring Security and its support for annotation-based declarative authorization.

Introduction

If you’re developing enterprise web applications, then you have certainly applied some form of security to your apps. In some cases it suffices to come up with a number of roles that you can assign to (groups of) users and to perform authorization based on that. However, in many cases that’s not enough and the concept of data access control comes into play: only users that are somehow related to the data they’re trying to work with should be allowed to access that data. This can be through direct ownership, access control lists, some temporary relation like a doctor-patient treatment relationship, etc. A common requirement in multi-tenant applications, where a single application instance is used by people from different organizations whose data should be strictly separated, is that data should only be accessible by people who work for the organization that that data belongs to.

In this blog we’ll show you one approach that we used to implement this with Spring Security.

Requirements

What we want is to ensure in our MVC controllers that users cannot access data they’re not allowed to. Most of our URLs are RESTful (according to some loose definitions of REST at least) and thus contain IDs of requested resources as path variables. Up front we don’t know much about these resouces: we first have to retrieve them from some repository in order to determine what organization they belong to and if that’s the same organization that the current user belongs to.
One way to do this would be programmatically. Let’s say we’re dealing with a ‘project’ resource. A request to /projects/4 might be handled like this:

@Controller
@RequestMapping("/projects")
public class ProjectController {

  // dependencies etc. skipped for brevity

  @RequestMapping(value = "{projectId:\\d+}", method = GET)
  public String getProject(Long projectId, Model model) {
    Project project = projectRepository.findOne(projectId);
    if (project == null) {
      throw new EmptyResultDataAccessException(
          "No project with id " + projectId, 1);
    }
    // getActiveUser() is a helper method
    if (!project.getOrganization().equals(
        getActiveUser().getOrganization())) {
      throw new AccessDeniedException(
          "Project does not belong to your organization");
    }
    // add the project to the model and return some view
    // ...
  }
}

This mixes an authorization concern into all of our controller methods. Moreover, it’s typically exactly the same code that we need in every method related to a project.

Our requirements were to:

  1. not duplicate the authorization checks everywhere
  2. express the constraints declaritively instead of through code as far as possible

Checking in a single place

To start with the first requirement: for our example that means the for all methods that involve a project ID path variable, we want a single place to check if the corresponding project belongs to the user’s organization.
We ended up using an @ModelAttribute annotated method for that: this method is invoked before every @RequestMethod annotated method in our controller, and as such provides a good place to perform an authorization check before handling the request.

But how do you access the project ID path variable in this method? You might be tempted to write something like this:

@ModelAttribute
public void checkProject(@PathVariable("projectId") Long projectId) {
  // load the project and compare its organization with
  // the current user's
  // ...
}

The problem with this approach is that not every method might define a projectId path variable. For example, a method that lists all projects of the current organization doesn’t have a project ID in its URL. The @PathVariable annotation doesn’t support something like a required attribute that you could set to false, so we have to use a different approach (BTW, I’m considering to file a feature request for exactly this purpose).
What you can do is to use this annotation to pass a map with *all* path variables to your @ModelAttribute method and then extract the variable yourself like this:

@ModelAttribute
public void checkProject(@PathVariable Map<String, String> vars) {
  if (vars.containsKey("projectId")) {
    Long projectId = Long.valueOf(vars.get("projectId"))
    // load the project and compare its organization with
    // the current user's
    // ...
  }
}

Now all you have to do is to have each method in your controller that has a path variable which is a project ID use the name “projectId” and you have your check.

Solution to have the check declaratively

As stated, we’d rather have Spring Security do the check and throw the corresponding exception if the user tries to access another organization’s project(s). A good way to define these kinds of constraints is to use the @Pre- and @PostAuthorize annotations, which allow you to refer to a method’s parameters and returned object through a Spring Expression Language statement (SpEL for short). In addition to that, the current principal that’s contained in the current Authentication from the SecurityContext is accessible as a ‘principal’ variable.
An @ModelAttribute method can return an object, which is then added to the model under a provided or generated key name. That means we can do this:

@ModelAttribute
@PostAuthorize("returnObject == null ||
    returnObject.organization.id == principal.organization.id")
public Project checkProject(@PathVariable Map<String, String> vars) {
  Project project = null;
  if (vars.containsKey("projectId")) {
    project = projectRepository.findOne(
        Long.valueOf(vars.get("projectId")));
    if (project == null) {
      throw new EmptyResultDataAccessException(
          "No project with id " + vars.get("projectId"), 1);
    }
  }
  return project;
}

The additional benefit of this approach is that the model now contains the Project instance that we looked up using our repository, so it can be passed directly to the relevant @RequestMapping methods:

@RequestMapping(value = "{projectId:\\d+}", method = GET)
public String getProject(Project project) {
  // project is already loaded and put in the model
  return "project/changeProject";
}

Building upon this principle

I showed you one example of how to use the @PostAuthorize annotation to implement the ownership authorization check, but this idea can be applied in other even more powerful ways as well.
In our example, a project contains multiple Sources. Users work with these sources through a controller, and as sources uniquely belong to a project the controller looks like this (all source-related URLs are relative to their owning project):

@Controller
@RequestMapping("/projects/{projectId}/sources")
public class SourceController {
  // contents left out
}

That means that every method is guaranteed to contain a projectId path variable. We can have an @ModelAttribute annotated method similar to the previous example that populates the model with that project and performs the ownership check. Now let’s take a method that’s used to delete a number of sources from a project. That controller method receives a form that holds the actual Source instances that should be deleted, but we want to ensure that these sources really belong to the project in the URL: otherwise, users could delete sources from other projects simply by hacking the POST request parameters.

Here’s how we implemented that:

@RequestMapping(params = "delete", method = POST)
@PreAuthorize("#form.sources.?[project.id != #project.id].empty")
public String deleteSources(Project project, SourcesForm form) {
  // iterate over form.getSources() and delete them
  // ...
  return "redirect:.";
}

Wow, what’s that? SpEL is actually quite a powerful language, and includes support for collection selection through the ‘?’ operator in addition to things like property access through the dot operator. What this expression states is: the list of sources in the form that do not belong to the given project should be empty.

Spring Data’s DomainClassConverter

BTW, if you know something about Spring-MVC binding you may be wondering how the form is populated with Source instances when the client only provides source ids as request parameters. We’re using Spring Data JPA and have configured Spring Data’s DomainClassConverter. It’s the best thing since sliced bread: whenever your controller method expects some entity instance and is actually provided with a request parameter or path variable that holds the String representation of that entity’s primary key field, this converter will try to look up the entity using a Spring Data repository. Your method will then receive the retrieved instance, or null if there was none for the given ID. This allows CRUD operations to become completely trivial to implement in your controllers. Here’s an example for an Instruction entity that needs to be updated (we don’t have ownership check requirements here, so no @ModelAttribute method is used):

@RequestMapping(value= "instructions/{instruction}", method = POST)
public String updateInstruction(
    @Valid Instruction instruction, BindingResult result) {
  // check for errors and then process the updated instruction
  // ...
  return "redirect:.";
}

See how the path variable name matches the parameter name? Spring Data will now retrieve the instruction with the ID given in the URL and Spring-MVC then binds the request params on that instance and passes the updated instance to your controller. Note that this works for method parameters annotated with @PathVariable as well, but in that case no binding will occur (which is often what you want when handling GET methods or for path variables occurring earlier in the URL).

Conclusion

Using Spring Security’s support for annotation-based declarative security with SpEL expressions in combination with @ModelAttribute or @RequestMethod annotated Spring-MVC controller methods provides an easy yet extremely powerful way to implement authorization access constraints. Combined with other measures, like role-based security and limiting the properties that can be bound based on request parameters, this makes it a complete and non-intrusive security solution that keeps your code clean, DRY and testable. Add Spring Data’s DomainClassConverter into the mix and your controllers will be as lean and mean as they can be!

2 Responses

  1. September 22, 2013 at 18:02 by Per Olesen

    Nice and declarative, I agree.

    Stepping back a little, do you also find solving this problem in the controller the “best way”? I would’ve thought it to be much nicer to have it solved all the way down in the data-access stack or maybe all the way down in the database, if supported there.

    If the data access stack (or even better, database) is isolating your tenants’ data for you, there is no need to clutter the code with access control logic on domain objects. Also, some code might “forget” to do the proper checks.

    Do you have any experiences on solving it at that level?

  2. October 6, 2013 at 00:17 by Joris Kuipers

    I hate our blog system: I never saw your comment until I stumbled upon it by accident. Sorry, I wasn’t ignoring you.
    Yes, I do have experience in solving this at a lower level. In the past I’ve achieved good results by using parameterized Hibernate Filters that automatically selected data based on some type of ownership derivable from the current user. Find by primary key won’t take filters into account though, so this can be a bit tricky. Recently I found out that the Hibernate guys actually turned this into a feature: have a look at http://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html for details.
    In this particular application I chose to go down a different path but still needed to make sure that users wouldn’t be able to hack their URLs to access data from another tenant. In that case the described approach works pretty well.