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.
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?
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.
While this was a simple implementation, it illustrated a few key benefits. The Finder pattern:
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.