Skip to content

jcoglan/consent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Consent

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.

Installation

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

Request expressions

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.

Expression grammar

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? }

HTTP method filtering

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

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.

Be careful with allow and deny

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? }

Redirects

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

Throttling

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 or per_month on an integer. You can also simply use per, 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.

Helper methods

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 and session provide access to the objects of the same name that are commonly accessed in controller code.

  • format returns a StringInquirer for testing the response format. For example, format.json? returns true if JSON format has been requested.

  • development?, production? and test? each return true or false 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? }

Extra: request expressions aren’t just for your consent.rb

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

About

Access control layer for ActionPack, providing a DSL for writing a firewall to sit in front of Rails controllers

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages