Rails #6: Grant edit access to select users

In my last article, I showed how to implement authentication with Ruby on Rails. But security is about more than simple login. For many applications, we want to grant permission to manipulate a resource to a set of users. In this article, I will guide you though adding functionality so that users can modify the permissions for who gets to edit an article.

Before we start implementing access control, we should get our application ready by encapsulating all access control checks. First, in app/helpers/login_sessions_helper.rb, change current_user_can_edit to the following (WILL BREAK):

This means that we need to implement User.can_edit?. For now, just use the previous code from the helper:

Finally, make sure we use the same access check in app/controllers/articles_controller.rb (notice that the filters for edit, update and delete require a user who can edit the article, but new and create only checks that the user is logged in):

Implement redirect_to_loginapp/controllers/application.rb:

Finally, changes all places in app/controllers/articles_controller.rb where we fetch articles through the current user. The security check that these provided is now obsolete. Search for current_user.articles and replace with Article (if you oode reads current_user.articles.build, the new code must say Article.new).

Our application logic is now ready for custom permissions.

Creating a permissions model

I like to think of permissions as resources, just like articles, comments and users. However, since we may potentially want to add many permissions at the same time, we need a custom controller. This will be a basic lesson on creating a somewhat strange controller to edit a many-to-many relationship. Here are the steps needed:

  1. We start by creating the model for the permissions: ruby script/generate model permission editable_article_id:integer editor_id:integer
  2. Enter the migration into the database: rake db:migrate
  3. Create the controller. Let’s just give it a few actions: ruby script/generate controller permissions show edit create.
  4. Make the controller a nested resource of the articles. In config/routes.rb, add the following inside the map.resources :articles section: article.resource :permissions, :name_prefix => nil (notice the singular form for “resource”)
  5. http://localhost:3000/articles/article_id/permissions/ should now take us to the show page for the permissions of an article. It’s currently empty. This is how we want it to look (THIS WILL FAIL):
  6. In order to get the @article object, put the following in app/controllers/permissions_controller.rb‘s definition of show: @article = Article.find(params[:article_id]).
  7. For this to work, Article.editors must be implemented. First, associate articles with permissions: has_many :permissions, :foreign_key => 'editable_article_id' (notice that the foreign_key option matches what we put in the migration). Second, make the permission know about editors: In app/model/permission.rb: belongs_to :editor, :class_name => "User" (Notice that we have to specify the class name, as it is not the same as the association). Lastly, make articles associated with editors through the permission association: has_many :editors, :through => :permissions. If you refresh the permissions page, it should now give an empty list, as expected.
  8. For the edit page, we want to display all users with a checkbox that shows whether that user has a permission to edit the article or not. Here’s my code for app/views/permissions/edit.html.erb:
  9. To get the code to display correctly, implement app/controllers/permissions_controllers.rb edit as follows: @article = Article.find(params[:article_id]).
  10. So far, so good. Now comes the hard part: Updating the association on the post. The following is inspired from Ryan Bates’ excellent screencast on many-to-many associations with checkboxes. First, notice how the name of the check_box_tag ends with “[]”? This is to let ail know that we intend for all “article[editor_ids]” values to come in as an array. If you take a look at the application log, you will see something like this: Parameters: {"article"=>{"editor_ids"=>["618895042", "618895044", ..... Second, a has_many :editors association will generate an attribute for editor_ids, as well an attribute for editor. Put these to facts together, and you get the following simplified implementation of PermissionsController.update:
  11. Aaaaand, we’re done!
  12. Well, not quite – there’s a gaping security hole. Can you see it? If not, don’t worry, we’ll get back to it.

Using the new security features

Since we prepared our application for the new security implementation, getting it to take effect is extremely simple, just update app/model/user.rb‘s can_edit? method to the following: article.user_id == self.id or editable_articles.include? article. In order for this to work, we need to create a relationship from user to articles, like we did for the other direction:

  1. class User has_many :permissions, :foreign_key => :editor_id
  2. class Permission belongs_to :editable_article, :class_name => 'Article'
  3. And class User has_many :editable_articles, :through => :permissions

That’s it. We’re ready to test:

  1. Log in as a user who owns an article
  2. Navigate to the article. The edit link should be visible.
  3. Add permissions to the URL (you might want to create a link_to this page!)
  4. Click the Edit link
  5. Give a few users permission to edit the article by checking their names and clicking submit
  6. Log in as a different user who now should have edit permissions for the article
  7. Navigate to the article. The edit link should now be visible.
  8. Outstanding

Who watches the watchmen?

Have you found the security hole yet? The problem is simply that any user can modify the permissions for an article, giving themselves access to edit the article. We could fix this by protecting the permissions like we protect the articles, but instead, I want to demonstrate how to use multiple permission schemes.

  1. We start by adding a much wanted link. In app/views/articles/edit.html.erb, add the following (WILL BREAK): < % if current_user.can_admin? @article %>< %= link_to 'Change editors', edit_permissions_path(@article) %>< % end %>.
  2. This will break, because we need to let app/model/user.rb implement the can_admin? method. Here’s a possible implementation: def can_admin? article; article.user_id == self.id; end. We could imagine other rules for this, for example with a different set of permissions for administrating an article, or a field on the permissions that determined the access level.
  3. To avoid the user from manually entering the URL, make sure to protect app/controllers/permissions_controller.rb as well:
  4. Protect the edit link in app/views/permissions/show.html.erb as well: < % if current_user and current_user.can_admin? @article %>< %= link_to "Edit", edit_permissions_path(@article) %>< % end %>
  5. This effectively protects the permissions from being edited by anyone except the original author of the article

A detour to increase our speed

When you start creating more Rails applications, you will quickly notice that the place where performance bottlenecks occur is with the access to the database. In our example, changing the permissions is particularly prone to being slow, as it currently fetches the user for every checked line it displays.

In order to prevent this, we can include the user information when looking for the permissions. The way to do this is to specify the extra information to be fetched right away on the Article.find. It basically looks like this: Article.find(params[:article_id], :include => :editors). :include is a powerful feature with lots of options. Make sure you understand it, or your applications will be slow.

A the end of our journey?

This article completes the construction of our blog application. Along the course of the set of articles, we have implemented one-to-many relationships, RSS, AJAX, learned about sessions and authentication, looked at many-to-many relationships in order to implement access control.

These articles show you much of the power of Rails as a web framework. However, we haven’t touched any of subjects needed to have an effective development process with Rails. Rails is often used together with Capistrano, which gives support for actually deploying applications to a server. In addition, we have only scratched the surface of the test options that exist for Rails.

This series of articles is at an end, but I am considering writing a few article on practical testing and deployment with Rails. Stay tuned for more Rails goodness.

About Johannes Brodwall

Johannes is Principal Software Engineer in SopraSteria. In his spare time he likes to coach teams and developers on better coding, collaboration, planning and product understanding.
This entry was posted in Ruby-on-Rails. Bookmark the permalink.

Comments are closed.