Consent is an access control abstraction layer for ActionController. It lets you restrict access to actions across your application using a single config file, rather than scattering access logic across your controllers using verify() and method-specific logic. It acts as a single before_filter to all your controllers and checks whether an action can run according to the rules you’ve set. Think of it as a firewall that sits between your routes file and your controller logic.
ruby script/plugin install git://github.com/jcoglan/consent.git
On installation, Consent adds a file to your app at config/consent.rb
. This file is where you write the access control rules for your application. You should see a block beginning:
Consent.rules do
You should write all your rules inside this block. Each rule is made up of two parts: a request expression, and a block that should return true
if the request is allowed, and false
if it should be denied. For example, here’s a simple rule that restricts requests to SiteController#hello
to only use Ajax:
site.hello { request.xhr? }
Writing rules is covered in some detail below.
Consent also adds some helpers to your controllers and routes file. To keep your routes working, you need to remove the map
parameter in config/routes.rb
. So:
# in config/routes.rb # replace ActionController::Routing::Routes.draw do |map| # with ActionController::Routing::Routes.draw do
Finally, add the Consent before_filter
to the top of your ApplicationController
to enable the Consent firewall for incoming requests:
# in app/controllers/application.rb class ApplicationController < ActionController::Base before_filter :check_access_using_consent # other filters, settings, etc... end
The first part of any rule is called the expression list. This is a snippet of Ruby code that is used to match requests to the controllers in your application. The expression language allows for matching of controllers, actions, parameters, response formats and HTTP methods, and provides a declarative method for referring to your app’s actions.
The basic grammar for expression lists is:
list ::= expression [+ expression]* expression ::= controller[action]?[params]?[format]? controller ::= name[/name]* action ::= .name params ::= (:name => value[, :name => value]*) format ::= [.|*]name name ::= [a-z][a-z_]* value ::= integer|string|regexp|range
That might seem a little abstract, so here are some examples. In the right-hand column, a symbol refers to a parameter, e.g. “:id” should be read as “params”.
Expression Requests matched --------------------------------------------------------------------------------------- site.hello SiteController#hello users All UsersController actions profiles.edit(:id => 12) ProfilesController#edit where :id == 12 pages(:id => "foo") All PagesController actions where :id == "foo" site(:name => /foo/i, :id => 4) SiteController, :name contains "foo", :id == 4 ajax/maps All actions in Ajax::MapsController ajax/maps.find Ajax::MapsController#find admin/users.search(:q => 4..8) Admin::UsersController#search, :q between 4 and 8 tags.list.json TagsController#list where the format is JSON tags*xml Any TagsController action where the format is XML tags(:q => "lolz")*txt TagsController, :q == "lolz" and format is TXT
Expressions are combined into a list using the +
operator. For example, the following complete rule matches SiteController#hello
and all PagesController
actions where :id
== "foo"
, and restricts them so that they can only be accessed using GET requests and a :name
parameter:
site.hello + pages(:id => "foo") { request.get? and !params[:name].nil? }
You can also use HTTP verbs (get, post, etc) in your expressions to match more specific requests. Each HTTP method takes a list of one or more expressions and narrows the scope of the expressions to a specific verb. For example, this matches all UsersController
actions using any verb, POST requests to ProfilesController#update
, and PUT requests to TagsController
and SiteController#hello
:
users + post(profiles.update) + put(tags + site.hello)
Decision blocks always appear at the end of a rule, and should return true
if the request is allowed and false
if it should be denied. Within the block, you have access to the request
, params
and session
objects so you can use them to make decisions about whether to allow the request.
Within a decision block, you can use the words deny
and allow
to clean code up a bit. Both these keywords cause the block to return early without processing any other instructions; deny
denies the request and allow
, well, allows it. For example, the following rule blocks requests for :id
between 45 and 60, except for if :id
is 54:
users.update(:id => 45..60) do allow if params[:id].to_i == 54 false end
If :id
is 54, the rule allows the request; allow
makes the block return early so it does not return the value false
from its last expression.
Note that allow
and deny
are only intended for returning early from rule blocks, and are provided because you can’t actually use the return
statement in Ruby blocks without running into some weird differences between procs, blocks and lambdas. You might be tempted to use them to write more expressive single-line rules, but this can lead to ambiguous rules. For example:
# Rule A tags.create { allow unless request.get? } # Rule B tags.create { deny if request.get? }
These look like they might do the same thing, but they don’t. In the face of a GET request, rule A
won’t call allow
and will simply return nil
, while rule B
will call deny
and block the request. Since deny
is a stronger command than allow
(requests are allowed by default!), Consent ignores a nil
response to a rule as this makes sure deny
rules like B
work all the time.
In short: if you want a request to be blocked, the rule must call deny
or redirect
, or evaluate to false
. Anything else will be ignored. The best way to write the above rules is:
tags.create { !request.get? }
You can also perform redirects from rule blocks using the redirect
keyword. Again, this keyword blocks execution of the rest of the block, and it allows you to use the same shorthand for action expressions as is used for matching requests (see above). For example, here’s a simple rule to block all requests to ProfilesController
, redirecting to SiteController#hello
:
profiles { redirect site.hello }
Note that if you only specify a controller with no action to redirect to, the Rails convention is to use the index
action. For example, this rule redirects to UsersController#index
if there is no user logged in, otherwise the request is allowed:
profiles do redirect users unless session[:user] allow end
Rule blocks can be used to throttle traffic to actions using identifier keys. For example, you may have a controller that exposes a web service API to your application, and users need to use an API key to make requests to it. If you wanted to limit each client to a maximum of a thousand requests per day to ApiController
, you could write:
api { throttle params[:api_key], 1000.per_day }
This monitors incoming requests matching the expression api
, splits the traffic up by the value of params[:api_key]
, and makes sure that no one key can make more than 1000 requests in any 24-hour period. You’re saying, “Each value of params[:api_key]
can make 1000 requests per day to ApiController
”.
In general, you’ll want to throttle on a per-client basis (where “client” could mean an API key, a user in the database, an IP address, a referring URL, etc) but if you want a global throttle just put in a constant value:
graphs.generate { throttle :all, 200.per(15.minutes) }
This means that GraphsController#generate
will fulfill no more than 200 requests in any 15-minute period in total for all incoming traffic, not just per user.
The full throtlling API is as follows:
-
throttle
takes two arguments. The first must be a string or symbol, and the second must be a rate expression. -
Rate expressions are generated by calling
per_second
,per_minute
,per_hour
,per_day
orper_month
on an integer. You can also simply useper
, as shown above, which takes a number of seconds.
Here are some more examples. Don’t take these as gospel as being the best way of doing various things, they’re only meant to make the API clearer…
# Slow down naive dictionary attacks from single machines users.login { throttle request.remote_ip, 1.per(5.seconds) } # Each subdomain can serve 500/day profiles*html { throttle request.subdomains.first, 500.per_day } # Stop Digg taking your site down pages.expensive_action { throttle request.referrer, 20.per_second } # Slow site down for one particular user photos { throttle :all, 3.per_hour if session[:user] == "joker" }
Note the last one throttles all requests for one chosen user, rather than for all users. You’re calling throttle
conditionally based on the session data, rather than creating a blanket rule for all users.
Also note that at present Consent stores request history in memory, so memory usage will increase if you use long time periods. This feature has not been load-tested in production yet, but if people report problems I’ll consider storing requests in SQLite.
To make it easier to write clean rules and reduce repetition, Consent allows you to define helper methods in the rule block that you can then use within rules to make decisions. For example, let’s say we want a method to grab the current user from the session:
helper(:user) { User.find(session[:user_id]) }
We can then use this helper in our rules:
profiles.update { user && user.is_admin? }
Consent provides a few built-in helpers to access commonly used data for performing access control. They are as follows:
-
request
,params
andsession
provide access to the objects of the same name that are commonly accessed in controller code. -
format
returns aStringInquirer
for testing the response format. For example,format.json?
returnstrue
if JSON format has been requested. -
development?
,production?
andtest?
each returntrue
orfalse
in response to the environment the app is currently running in.
For example, if you want DebugController
accessible only during development, this rule will do just that:
debug { development? }
For the sake of being extra specially helpful, Consent gives you the ability to use the request expression language described above in your controllers, views, and in your routes file. For example, you can map a route like so:
# Shorthand expression for # map.connect "foo", :controller => "tags", :action => "list", :format => "xml" map.connect "foo", tags.list.xml
Or, you can use expressions with methods that expect URLs in controllers and views:
# Redirects to :controller => "users", :action => "create", :name => "jcoglan" redirect_to users.create(:name => "jcoglan") # Opens a form whose action is :controller => "posts", :action => "update" form_for :post, :url => posts.update
These expressions make heavy use of method_missing
and operator overloading, so if you really care about performance or you find they cause any other problems, go into this plugin’s init.rb and comment out any line that looks like:
include Consent::Extensions::Xxx
Copyright © 2009 James Coglan, released under the MIT license