homeASCIIcasts

188: Declarative Authorization 

(view original Railscast)

Other translations: Cn Es

This episode is going to cover authorization, specifically role-based authorization. With this a user can be assigned to a role which gives them certain permissions throughout an application. Authorization is a tricky subject to cover, not least because there are so many different authorization plugins and gems available. Another problem is that the best authorization solution depends on the type of application you’re writing.

This episode will focus on declarative authorization which is one of the more advanced solutions. At first it might appear to be a rather complicated but with a little effort you’ll discover that it provides a lot of powerful functionality and can handle a wide range of authorization needs.

Adding Authorization to Our Application

In this episode we’ll be working with a blogging application. The application’s home page shows a list of articles and each article can have a number of comments which are shown on that article’s page.

The home page of our blogging application.

In its current state any user who visits the site can edit or delete any of the articles or comments which is obviously not what we want so we’ll be modifying the application to give each user a different level of functionality according to their role. We already have authentication set up on our site and there are links at the top of each page to allow users to sign up or log in. For this we used Authlogic, which was covered back in episode 160 [watch, read].

To add roles we’re going to create a new model called Role which will have a string attribute called name. The Role and User models will be linked by a has_many :through relationship via a join model called Assignment.

To assign roles to a user we’ll modify the sign-up form so that a user can choose their roles. This is done with a series of checkboxes using the technique described back in episode 17 [watch, read]. Obviously in a real world application we wouldn’t let users to choose their own roles but to keep this example application simple we’re going to allow this.

The signup page showing the roles checkboxes.

Our three roles will have different permissions: members of the admin role will be allowed to create, edit and destroy anything across the application; moderators will be allowed to edit anyone’s comments, but not articles; authors will be allowed to create articles and edit any articles that they have created and finally, users without any assigned role will be allowed to create comments and edit their own comments.

Getting Started With Declarative Authorization

Now that we know what we want to do let’s dive in and get started. Declarative authorization is available as a gem from Gemcutter so the first step we need to take is to add a reference to it in our application’s /config/environment.rb file.

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

Then, to make sure that the gem is installed we’ll run

sudo rake gems:install

The permissions for the roles we’re defining for our application are going be defined in a new file called authorization_rules.rb in the /config directory. Declarative Authorization provides its own DSL for defining roles and permissions and this is how we’ll use it to define the permissions for the admin user.

authorization do
  role :admin do
    has_permission_on [:articles, :comments], :to => [:index, :show, :new, :create, :edit, :update, :destroy]
  end
end

The roles are defined within an authorization block. Each role has a name and also takes a block in which that role’s permissions are set out. We use the has_permission_on method to define the permissions and this takes two parameters. The first is a list of the models that the role can access and this is followed by a :to parameter which has a list of the permitted actions on those models.

We’ll define the other roles later and test what we’ve done so far now. Before we go any further though we’ll need to make one other change as declarative authorization doesn’t know how our roles are defined. We need to make a change to our User model, adding a role_symbols method that returns a list of the roles the user has as an array of symbols. If, for example, our User model had an admin attribute we could write this method this way so that the user belongs to the :admin role if they are an admin.

def role_symbols
  [:admin] if admin?
end

Our application is different is that it has a many-to-many relationship between User and Role and a user’s roles are defined when they sign up through the checkboxes we saw earlier. So in role_symbols we’ll convert the current user’s roles into an array of symbols which is done like this:

def role_symbols
  roles.map do |role|
    role.name.underscore.to_sym
  end
end

Declarative authorization will now be able to map a user’s roles to the roles we’re defining in the authorization_rules file.

There are a couple of steps we still need to take. One is to set up a before_filter in our ApplicationController to tell declarative authorization what the current user is.

class ApplicationController < ActionController::Base
  helper :all
  protect_from_forgery
  before_filter { |c| Authorization.current_user = c.current_user }
end

Finally we need to go into each of our controllers and add a before_filter there to restrict access. We do this by calling a method that declarative authorization provides called filter_resource_access. This method adds before filters to RESTful type controllers and also does one other thing: when filter_resource_access is called it loads the single resource model so that it can check the access on it. This means that in each of the show, new, create, edit, update and delete actions we can remove the line that loads the model. We can do this in both the ArticlesController and the CommentsController in our application. If any of your controllers don’t follow the RESTful conventions then the documentation7 has details on how to deal with this.

Testing What We’ve Done

It has taken quite a bit of work to get this far but we’re finally at the stage where we can test what we’ve done so far. If we log in as a user with an admin role we can view the articles and the links to edit or delete articles and comments are visible. So far, so good, but when we log out though we see something unexpected:

The error page shown when trying to access an unauthorized action.

This happens because the application redirects to the home page, the articles index page, when we log out and as a guest we don’t have permission to view that action. To fix this we need to add the permissions for the guest in the authorization_rules file. The guest role is a special role reserved for users who are yet to sign up or log in or users who don’t have a role assigned to them.

authorization do
  role :admin do
    has_permission_on [:articles, :comments], :to => [:index, :show, :new, :create, :edit, :update, :destroy]
  end
  role :guest do
    has_permission_on :articles, :to => [:index, :show]
    has_permission_on :comments, :to => [:new, create]
  end
end

In the guest role we give permission for guests to access the index and show actions for articles and the new and create actions for comments. With these changes in place when we reload the page we can see the list of articles again.

We can now see the home page again.

Hiding Links to Forbidden Actions

Although guest users cannot edit or destroy articles and comments the links for these actions are still shown. Our next step therefore is to remove these links for those users in roles that don’t have permission to execute the actions they link to. We’ll have to modify the view code to do this and declarative authorization provides a useful method called permitted_to? to help with this.

In the view code for the ArticleController’s show action we’ll use permitted_to? to hide the links to edit or delete an article unless the current user has the appropriate permissions.

<p>
  <% if permitted_to? :edit, @article %>
    <%= link_to "Edit", edit_article_path(@article) %> |
  <% end %>
  <% if permitted_to? :destroy, @article %>
    <%= link_to "Destroy", @article, :method => :delete, :confirm => "Are you sure?" %> |
  <% end %>
  <%= link_to "Back to Articles", articles_path %>
</p>

The permitted_to? method takes two parameters: the name of an action and a model. Further down in the same file we’ll make a similar modification so that the links are hidden unless the current user can edit or delete a comment.

<p>
  <% if permitted_to? :edit, comment %>
    <%= link_to "Edit", edit_comment_path(comment) %> |
  <% end %>
  <% if permitted_to? :destroy, comment %>
    <%= link_to "Destroy", comment, :method => :delete, :confirm => "Are you sure?" %>
  <% end %>
</p>

We also need to make a similar change in the index view to hide the new article link.

<% if permitted_to? :create, Article.new %>
  <p><%= link_to "New Article", new_article_path %></p>
<% end %>

Note that as we don’t have an instance of an article in the index action we’ve used Article.new instead.

When we reload the page now the edit and destroy links will be hidden unless the current user has the appropriate permissions.

The edit and destroy links are now hidden.

Adding More Roles and Rules

Now that everything is set up all we need to do to alter access to the different parts of the site is to make changes to the authorization_rules file. The DSL that’s provided allows us to set some fairly advanced rules. For example if we want to allow logged-in users who have no roles assigned to them to be able to edit their own comments we can add the following rule to the :guest role.

role :guest do
  has_permission_on :articles, :to => [:index, :show]
  has_permission_on :comments, :to => [:new, :create]
  has_permission_on :comments, :to => [:edit, :update] do
    if_attribute :user => is { user }
  end
end

Declarative authorization provides an if_attribute method which can be used to add conditions to a permission. In the new rule above permission is only granted if the :user attribute, that is the user the comment belongs to, is the currently logged in user.

If we now log in as a user with no roles and add a comment to an article we’ll see that we have an “edit” link for that comment but not the comments made by anyone else.

The guest user can now edit the comments they have made.

All we’ve had to do to enable this is make one change to the authorization_rules file which shows how easy it is to set up rules like this.

Adding Permissions For Other Roles

We have set up three roles in our application but so far we have only defined permissions for one of them (as well as the guest role). We’ll add permissions for those roles now.

role :moderator do
  includes :guest
  has_permission_on :comments, :to => [:edit,:update]
end
role :author do
  includes :guest
  has_permission_on :articles, :to => [:new, :create]
  has_permission_on :articles, :to => [:edit, :update] do
    if_attribute :user => is { user }
  end
end

Notice that we’ve used an includes method here to give one role the permissions of another. Any user in the moderator role can do anything a guest can do and also edit and update anyone’s comments. Similarly authors can do all that a guest can do but can also create articles and update any articles that they have written.

A Better Error Message

To finish this episode we’ll make one last change to our application. If someone viewing our site tries to visit an action they don’t have access to they’ll see a basic error page with a message saying that they aren’t allowed to do that.

We can improve this by modifying our ApplicationController. If we add a method called permission_denied it will be executed when an attempt to call an unauthorized action is made. So instead of showing a bare error we’ll have our application redirect to the home page and show an flash message.

def permission_denied
  flash[:error] = "Sorry, you not allowed to access that page."
  redirect_to root_url
end

Now when we try to access an action we’re not allowed to we’ll be redirected to the home page and shown a better error.

We now see a custom error when trying to access an unauthorized action.

That’s it for this episode. Declarative authorization provides a neat, powerful solution for authorization in Rails applications and we’ve only really scratched the surface of what it can do. The documentation pages provide much more details and are well worth reading if you want to delve a little deeper.