How to use rails routing constraints: 2 use cases with code

I’ve been using routing constraints a lot lately.

Before I learned about constraints, my controllers were littered with data typing checks and filters, some of my actions were huge cascades of if-else or switch statements and I nervously wondered what other rails security vulnerabilities are still overlooked.

But not anymore. I use routing constraints commonly to add security and cleanliness to projects I work on, and I think that more programmers using rails can benefit from this tool.

Introducing Routing Constraints

Routing constraints are checks written around routes that will clean and validate information before even hitting a controller action. The key word here is before. Before this external data can touch behavior in your controllers, and subsequently your persistence layers, it already gets vetted. This opens up a lot of flexibility and security to our controllers, and can keep us from writing more boilerplate than we normally would.

What about some use cases?

I learn the best from examples -- from context and code. I’ve chosen some of what I thought are the most valuable and enlightening use cases that highlight effective and efficient constraint usage.

1. Security vulnerabilities and data type validation

Lately the rails community had a flurry of excitement with the uncovering of a number of security holes for sql injection. Commonly-used frameworks are usually secure, but issues like this occur when the data directly submitted to a database query can be anything from the params. We’ve seen that its difficult to protect from literally everything. And you don’t need to if you set constraints at the route level. Our route looks like this:

1 get   'user/:id'   => 'user#show'

Lets say that this:

1 {:drop => drop * from users; --}

(something similar from the sql injection hole described here) sent in as a param, that when used directly by a dynamic finder like this:

1 User.find_by_id(params[:id])

would wipe out the user table (this security issue has already been addressed in newer versions of rails). If we are worried about users injecting something harmful through the params -- whatever that may be -- changing our route like this will prevent this from happening:

1 get   'user/:id'   => 'user#show', constraint: { id: /\d+/ }

If it doesn’t pass the integer test, we don’t hit the action. Problem solved. No need to have to_s or to_i in our actions and no need to worry about writing extra checks to plan for security holes. Constraints are best used for basic data validation, and not for more complicated business rules. But for doing basic data validation and url protection, a few routing constraints can remove a lot of duplicated type validation and make your system more secure.

2. Encapsulate and simplify actions (and supporting code)

Lets say we have a controller like this:

1 def action
2     if params['strategy']    == 'new_user'
3       #execute
4     elsif params['strategy'] == 'wants_photo'
5       #execute
6     elsif params['strategy'] == 'user_to_update'
7       #execute
8     end
9   end

and our route is like this:

1 get    'sample/url' => 'my#action'

With constraints, we can encapsulate these responsibilities and break each action off into its own by comparing what’s in the query parameters: Our routes.rb after:

 1 get    'sample/url' => 'my#new_user',       constraints: NewUserConstraint
 2   get    'sample/url' => 'my#wants_photo',    constraints: WantsPhotoConstraint
 3   get    'sample/url' => 'my#user_to_update', constraints: UserToUpdateConstraint
 4 
 5   class NewUserConstraint
 6      def self.matches?(request)
 7        request.query_parameters['strategy'] == 'new_user'
 8      end
 9   end
10 
11   class WantsPhotoConstraint
12      def self.matches?(request)
13        request.query_parameters['strategy'] == 'wants_photo'
14      end
15   end
16 
17   class UserToUpdateConstraint
18     def self.matches?(request)
19        request.query_parameters['strategy'] == 'user_to_update'
20     end
21   end

Of course in this example I need to pass in a strategy, and that is one way to do it. You could also replace the strategy with just the presence of a parameter:

1 class WantsPhotoConstraint
2     def self.matches?(request)
3       request.query_parameters['wants_photo'].present?
4     end
5   end

If the constraint passes, it will route to the correct action. So now our controller looks like this:

 1 def new_user
 2     #execute
 3   end
 4 
 5   def wants_photo
 6     #execute
 7   end
 8 
 9   def update_data
10     #execute
11   end

Much nicer.

Constraints are easy to incrementally implement in your application

I think these use cases are enough to show how constraints promote less boilerplate, less worry about security, better encapsulation, and more control over your system from an abstracted layer.

Start by looking for shared validation functions, or those pesky ‘to_i’ and ‘to_s’ functions that commonly append themselves to param calls in actions. Constraints are low-cost in terms of time and effort for implementation and yield nice results for the health of your system.

Ben Voss, Software Craftsman

Ben Voss is particularly interested in client-side technologies.