Everyday Rails

Adding authorization to your Rails app with RESTful_ACL, part 2: Basic ACLs

June 21, 2010

In part one of this series on the RESTful_ACL gem, I walked through the steps required to prepare an app to use this handy, albeit overshadowed, mechanism for adding authorization to a Rails application. Now let’s dive into the actual ACL settings themselves. In the interest of simplicity and familiarity, we’ll go with a basic blogging app with categories, posts, and comments. Let’s say that admins can create and edit categories, regular users can write posts and edit posts they’ve written. In this part I’ll cover categories and posts; we’ll use some more advanced features to protect our comments in a later tutorial.

Behind the scenes, I’ve set up a Rails 2.3.8 application with an authentication system installed. The important thing to know here is that your authentication session information must be named current_user, which has pretty much become practice in every authentication mechanism I’ve seen for Rails in the last couple of years. I’ve also added a boolean is_admin to my User model and run the generated migration.

First, let’s create the category and post scaffolds (see my post on using Nifty Generators to create Rails scaffolds if you need a primer):

  $ script/generate nifty_scaffold category name:string --haml
  $ script/generate nifty_scaffold post title:string body:text user_id:integer --haml

Run the migrations, then open the two new model files. Let’s start with the Category model, which only users where is_admin is true may manipulate. Its ACL settings are thus pretty simple:

app/models/category.rb:
  class Category < ActiveRecord::Base
    attr_accessible :name

    has_many :posts

    # ACL settings start here

    # Anybody may access the :index action
    def self.is_indexable_by(user, parent = nil)
      true
    end

    # Only administrators may access the :new and :create actions
    def self.is_creatable_by(user, parent = nil)
      user.is_admin?
    end

    # Anybody may access the :show action
    def is_readable_by(user, parent = nil)
      true
    end

    # Only administrators may access the :edit and :update actions
    def is_updatable_by(user, parent = nil)
      user.is_admin?
    end

    # Only administrators may access the :destroy action
    def is_deletable_by(user, parent = nil)
      user.is_admin?
    end
  end
  

For each of the five methods in the above model, returning true will allow the user to access the action in question. Otherwise they’ll get redirected to the denied path we set up in Part 1 of this tutorial, but first we need to tell the controller to refer to the ACL settings we just created:

app/controllers/categories_controller.rb:
  class CategoriesController < ApplicationController

    before_filter :login_required, :except => [ :index, :show ]
    before_filter :has_permission?
  
    # rest of controller ...
  end

Two things going on here: The first before_filter checks for current_user (in this case, I’m using Restful Authentication because it’s easy; other authentication mechanisms use before_filter as well but call different methods—consult their documentation). The second filter tells the controller that this model has ACLs to check. Assuming both of these filters return true then the requested action will process. Otherwise the user will be redirected to the login path (because :login_required returned false) or the denied path (because :has_permission?) returned false.

Let’s clean up a couple of views real quick. In our index view, we only want the links to each object’s Edit and Destroy options, and the New Category form, to be visible if the user has the appropriate permission (that is, is logged in and is an administrator).

app/views/categories/index.html.haml:
  - title "Categories"

  %table
    %tr
      %th Name
    - for category in @categories
      %tr
        %td= h category.name
        %td= link_to 'Show', category
        %td= allowed?{ link_to 'Edit', edit_category_path(category) } if logged_in?
        %td= allowed?{ link_to 'Destroy', category, :confirm => 'Are you sure?', :method => :delete } if logged_in?

  %p= allowed?{ link_to "New Category", new_category_path } if logged_in?

There are two things doing the work here—RESTful_ACL’s allowed? helper is checking the ACL, but only if Restful Authentication’s logged_in? helper returns true. This two-level setup is necessary because the :index action doesn’t require login, so some users (guests) may not be logged in.

You can also apply these helpers to show.html.haml to hide the Edit and Destroy links from non-administrators.

Now let’s move on to the Post model:

app/models/post.rb
  class Post < ActiveRecord::Base
    attr_accessible :title, :body, :post_id, :user_id

    belongs_to  :post
    belongs_to  :user

    # ACL settings start here

    # Anybody may access the :index action
    def self.is_indexable_by(user, parent = nil)
      true
    end

    # Only logged-in users may access the :new and :create actions
    def self.is_creatable_by(user, parent = nil)
      user != nil
    end

    # Anybody may access the :show action
    def is_readable_by(user, parent = nil)
      true
    end

    # Only the post's user or administrators may access the :edit and :update actions
    def is_updatable_by(user, parent = nil)
      user.eql?(self.user) or user.is_admin?
    end

    # Only the post's user or administrators may access the :destroy action
    def is_deletable_by(user, parent = nil)
      user.eql?(self.user) or user.is_admin?
    end
  end

The controller and views for the Post scaffold will be set up in the same fashion as they were for Categories.

We’re off to a good start. Our application now protects two models from unauthorized access. Next time I’ll add a basic commenting system to my application, and use RESTful_ACL’s logical parent option to extend existing ACLs to a model’s children. As always, please let me know what you think about this article. I appreciate your questions, comments and suggestions.

What do you think? Follow along on on Twitter or Facebook to let me know what you think and catch my latest posts. Better yet, subscribe to my newsletter for updates from Everyday Rails, book picks, and other thoughts and ideas that didn't quite fit here.

Black lives matter.

I stand with the Black community against systemic racism, police violence and brutality, intolerance, and hate in the United States and worldwide. We must all demand better from our leaders, and ourselves. Stop tolerating intolerance.

While you're here, please consider making a donation to Black Girls CODE, who do great, important work to provide opportunity to underprivileged girls interested in tech, or any organization working toward equity and safety for all, not just the privileged. Thank you.

Test with confidence!

If you liked my series on practical advice for adding reliable tests to your Rails apps, check out the expanded ebook version. Lots of additional, exclusive content and a complete sample Rails application.

Newsletter

Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.