Blog
July 29, 2020
Jeremy Dye

“Finding” Your Way to Better Security in Multitenant Rails Applications

Let’s say you’ve created a SaaS application that lets customers keep track of their vendors. Things are going well and you’re adding many customers. One day, a curious customer starts messing around with the IDs in URLs and stumbles across a vendor they shouldn’t have access to. This is Very Bad™, especially if your customers are relying on your application to store sensitive information!

You have now entered the wide world of Authorizations. We’ll spend this blog post looking at one piece of the Authorization puzzle: using the Finder pattern to ensure users only see the things they’re allowed to.

The problem, illustrated

Let’s say you’re building the application we alluded to in the previous paragraph. One may begin with a naive implementation such as:

class VendorsController < ApplicationController
  def index
    @vendors = Vendor.all
  end

  def show
    @vendor = Vendor.find(params[:id])
  end
end

This may work fine and dandy at first, but as soon as you add your second customer you’re going to have a problem: every user has access to every vendor. Let’s say you foresee this problem. You create a User model that users authenticate with, add a user_id column to Vendors, and update your controller like so:

class VendorsController < ApplicationController
  def index
    @vendors = Vendor.where(user_id: current_user.id)
  end

  def show
    @vendor = Vendor.where(user_id: current_user.id).find(params[:id])
  end
end

Great! You also remembered to add scoping to your show route. A+, problem solved, ship it. All is well, for a time...

A month later you’ve moved on to other features in the application. A new engineer on the team is tasked with adding an Agreement model to keep track of the obligations between customers and each customer's vendors. She implements the route for creating a new Agreement like so:

class AgreementsController < ApplicationController
  def new
    @agreement = Agreement.new
    @vendors = Vendor.all
  end
end

The team is busy, as startups are, and the change slips through to production. The support tickets start streaming in with customers asking about strange vendors showing up. No bueno. What’s an engineer to do?

Example usage

The Finder pattern is a simple concept: when you want to show something to a user, use a Finder. Finders implement a base scope upon initialization so that users only see what they’re allowed to. Subsequent methods chain off of that main scope, to ensure only a subset of the authorized scope is returned.

Let’s take a look at what a simple finder might look like for Vendors:

class VendorFinder
  attr_reader :scope

  def initialize(user_id)
    @scope = Vendor.where(user_id: user_id)
  end

  def by_id(id)
    @scope.find(id)
  end
end

And how it might be used in our controllers:

class VendorsController < ApplicationController
  def index
    @vendors = VendorFinder.new(current_user.id).scope
  end

  def show
    @vendor = VendorFinder.new(current_user.id).by_id(params[:id])
  end
end

class AgreementsController < ApplicationController
  def new
    @agreement = Agreement.new
    @vendors = VendorFinder.new(current_user.id).scope
  end
end

Finders are most frequently used in controllers, but they can - and should - be used everywhere you are scoping to an authorized subset of records. Part of the security benefit is that when it is used everywhere as a convention, engineers are less likely to customize their own scoping and make an error.

Conclusion

While this was a simple implementation, it illustrated a few key benefits. The Finder pattern:

  1. Provides a safe, consistent pattern for you to show things to the user;
  2. Requires the information it needs to provide that safety during initialization;
  3. Centralizes the logic of who should see what; and,
  4. Is easy to test.

Some might say that in lieu of the Finder pattern, authorization problems could be mitigated with better code review, documentation, or in our specific case, using a /users/:id/vendors route. But, without the Finder pattern, engineers will always need to be thinking through the complexities of each individual scenario: “What vendors can this customer see?” or “Is this User an admin that can see all Agreements?”, etc. With the Finder pattern, you can instead use the simple heuristic of “Will the user see this?” when writing or reviewing code.

The important concepts here are that applications grow complex, models can be accessed from a variety of contexts, expertise in a system is spread unevenly across a team, and humans just make mistakes sometimes. It’s up to us as engineers to put in guardrails and conventions to prevent these mistakes when we can, and the Finder pattern is yet another tool at our disposal at Aptible.