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):
def current\_user\_can\_edit article
current\_user and current\_user.can\_edit? article
end
This means that we need to implement User.can_edit?. For now, just use the previous code from the helper:
class User
# ...
def can\_edit? article
article.user\_id == self.id
end
end
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):
class ArticlesController < ApplicationController
before\_filter :can\_edit, :only => [ :edit, :update, :delete ]
before\_filter :verify\_logged\_in, :only => [ :new, :create ]
# ...
# All actions go here
private
def can\_edit
@article = Article.find(params[:id])
unless @article and current\_user and current\_user.can\_edit? @article
redirect\_to\_login
end
end
end
Implement redirect_to_login
app/controllers/application.rb:
class ApplicationController < ActionController::Base
# ...
def redirect\_to\_login
flash[:notice] = "You must login in to do this"
session[:redirect\_after\_login] = request.request\_uri
redirect\_to new\_login\_session\_path
end
end
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:
We start by creating the model for the permissions:
ruby script/generate model permission editable_article_id:integer editor_id:integer
Enter the migration into the database:
rake db:migrate
Create the controller. Let’s just give it a few actions:
ruby script/generate controller permissions show edit create
.Make the controller a nested resource of the articles. In
config/routes.rb
, add the following inside themap.resources :articles
section:article.resource :permissions, :name_prefix => nil
(notice the singular form for “resource”)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):Users who can edit < %= @article.title %> ----------------------------------------- < % for editor in @article.editors - [@article.user] %> * < %= editor.username %> (< %= editor.full\_name %>) < % end %> < %= link\_to "Edit", edit\_permissions\_path(@article) %>
In order to get the
@article
object, put the following inapp/controllers/permissions_controller.rb
’s definition ofshow
:@article = Article.find(params[:article_id])
.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: Inapp/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.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
:Permissions for < %= @article.title %> ====================================== < % form\_for :article, :url => { :article\_id => @article, :action => :update } do |f| %> < % for user in User.find(:all) - [@article.user] %> < %= check\_box\_tag "article[editor\_ids][]", user.id, @article.editors.include?(user) %> < %= user.username %> (< %= user.full\_name %>) < % end %> < %= f.submit "Update" %> < % end %>
To get the code to display correctly, implement
app/controllers/permissions_controllers.rb
edit
as follows:@article = Article.find(params[:article_id])
.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, ahas_many :editors
association will generate an attribute foreditor_ids
, as well an attribute foreditor
. Put these to facts together, and you get the following simplified implementation ofPermissionsController.update
:
```rails
def create
@article = Article.find(params[:article\_id])
@article.editor\_ids = params[:article][:editor\_ids]
if @article.save
flash[:notice] = 'Permission was successfully created.'
redirect\_to(permissions\_path(@article))
else
render :action => "edit"
end
end
```
- Aaaaand, we’re done!
- 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:
- class User
has_many :permissions, :foreign_key => :editor_id
- class Permission
belongs_to :editable_article, :class_name => 'Article'
- And class User
has_many :editable_articles, :through => :permissions
That’s it. We’re ready to test:
- Log in as a user who owns an article
- Navigate to the article. The edit link should be visible.
- Add
permissions
to the URL (you might want to create alink_to
this page!) - Click the
Edit
link - Give a few users permission to edit the article by checking their names and clicking submit
- Log in as a different user who now should have edit permissions for the article
- Navigate to the article. The edit link should now be visible.
- 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.
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 %>
.This will break, because we need to let
app/model/user.rb
implement thecan_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.To avoid the user from manually entering the URL, make sure to protect
app/controllers/permissions_controller.rb
as well:class PermissionsController < ApplicationController before\_filter :can\_admin, :except => [ :show ] # ... private def can\_admin @article = Article.find(params[:article\_id]) unless @article and current\_user and current\_user.can\_admin? @article redirect\_to\_login end end end
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 %>
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.