Skip to content

Tutorial: securing a controller

Tim Chambers edited this page Jan 7, 2015 · 44 revisions

Agenda: we’ve got a standard REST controller (ProductsController) and want to discover how access restrictions can be applied to it (using acl9 of course!)

Here’s the controller (with omitted action bodies for brevity):

class ProductsController < ApplicationController
  def index
    # ...
  end

  def show
    # ...
  end

  def new
    # ...
  end

  def edit
    # ...
  end

  def create
    # ...
  end

  def update
    # ...
  end

  def destroy
    # ...
  end
end

Install acl9

Add a line to the config/environment.rb file and install the gem.

config.gem "acl9", :source => "http://gemcutter.org", :lib => "acl9"

First step: use acl9 with no restrictions

When everyone is allowed to do everything, it’s also access control. So we’ll start from this.

All access rules in controllers are put into access_control block. The all-permissive variant will be:

class ProductsController < ApplicationController
  access_control do
     allow all
  end

  def index
    # ...
  end

  # ...other actions...
end

allow all makes sense here, doesn’t it?

This controller should work, if it doesn’t, you might have some problems with acl9 installation.

Second step: allow only logged in users

So, you actually don’t want your competitor to come and delete all your products, replacing them with theirs! A simple scheme could work: you’re the only registered user, you do the stuff, and others are just anonymous users with no rights.

How about this?

access_control do
   allow logged_in
end

Seems cool. Only logged in users can… but wait! Now nobody can even see your products (except you)! Something must be done:

access_control do   
   allow logged_in
   allow anonymous, :to => [:index, :show]
end

The second rule to the rescue. In this case anonymous users can see, but not create, edit, or destroy products.

Q. How acl9 knows the user is logged in or not?

Short answer: not controller.send(:current_user).nil?.

This means you should use some authentication solution (authlogic, restful_authentication, clearance, or roll out your own current_user implementation).

Note_: if the method is named differently (current_account, or even @current_jedi_that_shouldbe@), acl9 can be configured to handle that (see the docs).

Q. What happens when anonymous user goes to, say, /products/new?

Answer: Acl9::AccessDenied exception is raised.

You’ll probably handle this in the ApplicationController, in the following manner:

class ApplicationController < ActionController::Base
  rescue_from 'Acl9::AccessDenied', :with => :access_denied

  # ...other stuff...
  private

  def access_denied
    if current_user
      render :template => 'home/access_denied'
    else
      flash[:notice] = 'Access denied. Try to log in first.'
      redirect_to login_path
    end
  end    
end

Third step: here comes the admin

Let’s imagine your company has had a very successful period, the product count grows every day, but you’re still the only user who can add, or edit products in any way. This task should now be delegated, right? But you’re still afraid that some registered user will come and destroy (intentionally or occasionally) all products, ruining your work.

That is, you want to let other registered users create and edit products, but reserve the destroy for yourself, the admin.

Here you are:

access_control do   
   allow :admin
   allow logged_in, :except => :destroy
   allow anonymous, :to => [:index, :show]
end

Now only the admin can do everything, other registered users are not allowed to destroy.

Q. How acl9 knows someone is admin?

Short answer: current_user && current_user.has_role?(:admin, nil).

You’ll get the has_role? method in your User class if you put acts_as_authorization_subject inside it and create appropriate tables (see the docs).

Fourth step: product owners & managers

So far so good. Still, any logged in user can edit products now, even if another user has created them.

And your logged in customers… right! They can edit the desired product, lowering the price to some affordable value, say $0.01, and then buy it. Oh my!

We’ll fix this situation by using object roles.

access_control do
  allow all, :to => [:index, :show]
  allow :admin
  allow logged_in, :to => [:new, :create]
  allow :owner, :manager, :of => :product, :to => [:edit, :update]
end

What have we got here? Everyone can go for index and show actions (allow all means just that). Admin can do anything he wants to.
Other logged in users can create new products. But only product owner and product manager are allowed to edit/update.

What does it mean, :product? Is it a role? Nope. Basically it’s a reference to @product, the instance variable in the controller.

Does acl9 set @product variable automagically? Nope. You’ll need a before_filter for that.

class ProductsController < ApplicationController
  before_filter :load_product, :only => [:edit, :update, :destroy, :show]

  access_control do
    allow all, :to => [:index, :show]
    allow :admin
    allow logged_in, :to => [:new, :create]
    allow :owner, :manager, :of => :product, :to => [:edit, :update]
  end

  # ...

  private

  def load_product
    @product = Product.find(params[:id])
  end
end

In this case @product variable will be initialized before the access control checks are executed, and that’s exactly what we need!

So, what’s an object role? It’s a role (owner & manager here), tied to a specific object, with specific class and id.

Q. How do I assign a role to some user?

If you use acl9 builtin role subsystem, has_role! method is the thing. Let’s assign owner role in create:

class ProductsController < ApplicationController
  # ....

  def create
    @product = Product.new(params[:product])

    if @product.save
      flash[:notice] = 'Product created.'
      current_user.has_role!(:owner, @product)         # <--------- assign the role
      redirect_to(@product)
    else
      render :new
    end
  end

  # ....
end

You may unassign a role as well:

current_user.has_no_role! :owner, @product

A @product object might even have several owners this way!

When assigning a role on an object, acl9 will create an entry in the table roles for the given role name (owner), for the authorization_type (Product) and the authorization_id (@product.id). It will also insert a record in roles_users to link the id of @current_user to the role.

Fifth step: tightening the creation

So, any registered and logged in user can create his own products now. It’s sorta Web 2.0-ish when your customer creates a product that he wants to buy from you, but let’s explore how to implement a more traditional way to do things.

We want to allow product creation only to selected users, how do we do that? One solution is to introduce product_manager role:

before_filter :load_product, :only => [:edit, :update, :destroy, :show]

access_control do
  allow all, :to => [:index, :show]
  allow :admin
  allow :product_manager, :to => [:new, :create, :destroy]
  allow :owner, :manager, :of => :product, :to => [:edit, :update]
end

Destroy right also goes to product_manager. That’ll be enough, but acl9 provides us with a nicer variation:

before_filter :load_product, :only => [:edit, :update, :destroy, :show]

access_control do
  allow all, :to => [:index, :show]
  allow :admin
  allow :manager,         :of => Product,  :to => [:new, :create, :destroy]
  allow :owner, :manager, :of => :product, :to => [:edit, :update]
end

The manager of the Product class itself! Makes sense, because new and create actions operate on Product, not on its instances. Note that Product manager isn’t automatically a @product manager (a class role doesn’t always imply an object role).

So, you can assign roles not only for objects, but also for classes:

@good_user.has_role!(:manager, Product)
@bad_user.has_no_role!(:manager, Product)

Sixth step: that’s it!

We’ve successfully enforced access rules on our ProductsController and finally it qualifies to Mostly High Standards of Security (that’s a joke, you get it).

To top it off, I’ll show you another way to express the same set of rules in acl9 DSL:

before_filter :load_product, :only => [:edit, :update, :destroy, :show]

access_control do
  actions :index, :show do
    allow all 
  end

  actions :new, :create, :destroy do
    allow :manager, :of => Product
    allow :admin
  end

  actions :edit, :update do
    allow :owner, :manager, :of => :product
    allow :admin
  end
end

This may be called action-oriented approach (as compared with the role-oriented approach used before). These two can be mixed together though.

Questions? Suggestions?

You’re welcome to acl9-discuss Google Group.