How to Use Rails Routing Constraints: Two Use Cases with Code

How to Use Rails Routing Constraints: Two Use Cases with Code

Ben Voss
Ben Voss

January 12, 2013

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:

get 'user/:id' => 'user#show'
Lets say that this:

{: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:

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:

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:

def action
		if params['strategy'] == 'new_user'
				#execute
		elsif params['strategy'] == 'wants_photo'
				#execute
		elsif params['strategy'] == 'user_to_update'
				#execute
		end
end

and our route is like this:

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:

get 'sample/url' => 'my#new_user', constraints: NewUserConstraint
get 'sample/url' => 'my#wants_photo', constraints: WantsPhotoConstraint
get 'sample/url' => 'my#user_to_update', constraints: UserToUpdateConstraint

class NewUserConstraint
		def self.matches?(request)
				request.query_parameters['strategy'] == 'new_user'
		end
end

class WantsPhotoConstraint
		def self.matches?(request)
				request.query_parameters['strategy'] == 'wants_photo'
		end
end

class UserToUpdateConstraint
		def self.matches?(request)
				request.query_parameters['strategy'] == 'user_to_update'
		end
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:

class WantsPhotoConstraint
		def self.matches?(request)
				request.query_parameters['wants_photo'].present?
		end
end

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

def new_user
		#execute
end

def wants_photo
		#execute
end

def update_data
		#execute
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.