views:

36

answers:

3

Hi there,

I'm trying to get some basic authentication/authorization with devise/cancan with Rails. Rather than using roles like Ryan B's screencast and other examples around I'm trying to do something basic:

1 - A user can log in
2 - A user can only edit/destroy their own articles (no roles, you're either logged in and can create new articles and edit/destroy your own or you're logged out and you can only see articles and login)

I'm using devise for the first part and that's working well but I can't get the second part working with CanCan. The the edit and destroy links for the articles don't appear when you're logged in and the direct URL (e.g. /articles/3/edit) still allows even if the article is for another user.

My ability.rb is

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user

    if user.nil? 
      can :read, :all
    else
#      can :manage, :all #test - with this, all the edit/destroy links appear
       can :manage, Article, :user_id == user
    end
  end
end

articles_controller.rb:

class ArticlesController < ApplicationController

  before_filter :authenticate_user!, :except => [:index, :show] # for Devise
  load_and_authorize_resource


  # GET /articles
  # GET /articles.xml
  def index

    @articles = Article.all

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @articles }
    end
  end

  # GET /articles/1
  # GET /articles/1.xml
  def show
    @article = Article.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @article }
    end
  end

  # GET /articles/new
  # GET /articles/new.xml
  def new
    @article = Article.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @article }
    end
  end

  # GET /articles/1/edit
  def edit
    @article = Article.find(params[:id])
  end

  # POST /articles
  # POST /articles.xml
  def create
    @article = Article.new(params[:article])
    @article.user = current_user

    respond_to do |format|
      if @article.save
        format.html { redirect_to(articles_path, :notice => 'Article was successfully created.') }
        format.xml  { render :xml => articles_path, :status => :created, :location => articles_path }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @article.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /articles/1
  # PUT /articles/1.xml
  def update
    @article = Article.find(params[:id])

    respond_to do |format|
      if @article.update_attributes(params[:article])
        format.html { redirect_to(@article, :notice => 'Article was successfully updated.') }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @article.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /articles/1
  # DELETE /articles/1.xml
  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    respond_to do |format|
      format.html { redirect_to(articles_url) }
      format.xml  { head :ok }
    end
  end
end

and the view partial that lists articles _article_list.html.erb:

    <table>
      <tr>
        <th>Title</th>
        <th>Description</th>
        <th>User</th>
        <th></th>
        <th></th>
        <th></th>
      </tr>

    <% @articles.each do |article| %>
      <tr>
        <td><%= article.title %></td>
        <td><%= article.description %></td>
        <td><%= article.user_id %></td>
        <td><%= link_to 'Show', article %></td>
        <% if can? :update, @article %>
            <td><%= link_to 'Edit', edit_article_path(article) %></td>
        <% end %>
        <% if can? :destroy, @article %>
            <td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td>
        <% end%>
      </tr>
    <% end %>
    </table>

With this setup, the edit/destroy links in the view don't show up unless there's a blanket can :manage, :all, even can :manage, Article doesn't work. As I mentioned above, it also isn't restricting the actual actions as you're able to deep link straight to editing an article and it permits it.

I'm not sure what I'm doing wrong here. It would be great to get some help.

Thanks in advance
Jason

A: 

Your condition for matching a user id isn't quite right. It should be:

can :manage, Article, :user_id => user.id

The attribute you want to check is mapped to the value you want to check against.

Also, you are checking for user.nil? when it can't be nil because you've just initialised it. (Probably a symptom of having tried lots of things!)

Shadwell
Thanks for your response Shadwell. Yes, I'm sure there's a few things astray now after trying many different things. I adjusted as you suggested but still with the same result. I don't know if it's telling but if I do the long form of can :manage, Article do |article| article.try(:user_id) == user.id end I get the error "undefined method `user_id' for :index:Symbol"
Jason
A: 

Does your catch work? If you uncomment the can :manage, :all line will a user be able to edit his/ her post ( along with everyone else's of course )?

Have you tried changing, can :manage, Article, :user_id == user to

can :manage, Article do |article|
 article.try(:user) == user

I have never been able to get load an authorize to work- although I suspect that I was doing something wrong. To prevent someone from accessing the url directly, in your article's edit action, try adding this

 unauthorized! if cannot? :edit, @article
RobertH
Thanks RobertH. Yep, the catch works - edit/destroy show up when logged in and disappear when logged out. I've tried can with the block as you've shown above and I get and error "undefined method `user' for :edit:Symbol" with the details from the trace saying "app/models/ability.rb:14:in `block in initialize'".
Jason
I've also tried removing the "load_and_authorize_resource" and specifying "authorize! :read, @article" in the index action. The browser then reports a redirect loop. Not really sure what I'm doing wrong here. Is there a formal CanCan support forum?
Jason
Did you try pasting the code in? I'm not sure why you would get an error like that since "edit" is not used. Also, in case you didn't know about this here's a blog post that tells you how to set up CanCan with Devise http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/
RobertH
Also, you're not adding user_id to Article, you're adding user. Take a look at your Article controller, create action. This @article.user = current_user should be @article.user_id = current_user.id
RobertH
Thanks for the info RobertH. I managed to fix it. I'll post the answer here.
Jason
A: 

I managed to resolve my problem. I reset my environment (rvm - resintalled the gems and gemsets - ruby 1.9.2 and rails 3.0.0) and changed some of the code and all the issues I was having went away (redirect loop, view elements not changing based on being logged in, unauthorized controller actions still permissable). I've pasted ability.rb, articles_controller.rb, and _article_list.html.erb.

ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)
    if user
      can :create, Article
      can :read, :all
      can :update, Article, :user_id => user.id
      can :delete, Article, :user_id => user.id
    else
      can :read, :all
    end
  end
end

I guess it makes sense now but because only update and delete were supposed to be for the current user's articles, I split out the CRUD elements to be specific.

articles_controller.rb

class ArticlesController < ApplicationController

  before_filter :authenticate_user!, :except => [:index, :show]
#  load_and_authorize_resource # RESTful automated CanCam authorization - excludes non RESTful

  # GET /articles
  # GET /articles.xml
  def index
    @articles = Article.all
    authorize! :read, @articles


    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @articles }
    end
  end

  # GET /articles/1
  # GET /articles/1.xml
  def show
    @article = Article.find(params[:id])
    authorize! :read, @article

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @article }
    end
  end

  # GET /articles/new
  # GET /articles/new.xml
  def new
    @article = Article.new
    authorize! :create, @article

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @article }
    end
  end

  # GET /articles/1/edit
  def edit
    @article = Article.find(params[:id])
    authorize! :update, @article
  end

  # POST /articles
  # POST /articles.xml
  def create
    @article = Article.new(params[:article])
    @article.user = current_user
    authorize! :create, @article

    respond_to do |format|
      if @article.save
        format.html { redirect_to(articles_path, :notice => 'Article was successfully created.') }
        format.xml  { render :xml => articles_path, :status => :created, :location => articles_path }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @article.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /articles/1
  # PUT /articles/1.xml
  def update
    @article = Article.find(params[:id])
    authorize! :update, @article

    respond_to do |format|
      if @article.update_attributes(params[:article])
        format.html { redirect_to(@article, :notice => 'Article was successfully updated.') }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @article.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /articles/1
  # DELETE /articles/1.xml
  def destroy
    @article = Article.find(params[:id])
    @article.destroy
    authorize! :delete, @article

    respond_to do |format|
      format.html { redirect_to(articles_url) }
      format.xml  { head :ok }
    end
  end

  def by
    @user = User.find(params[:id])
    @articles = @user.articles
    authorize! :read, @articles
  end
end

load_and_authorize_resource works but I've put specific authorize! lines in each controller action as I have an extra action at the bottom. Both now work.

I updated the reference to @article to article to reference the current article in the list in _article_list.html.rb:

<table>
  <tr>
    <th>Title</th>
    <th>Description</th>
    <th>User</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @articles.each do |article| %>
  <tr>
    <td><%= article.title %></td>
    <td><%= article.description %></td>
    <td><%= article.user_id %></td>
    <td><%= link_to 'Show', article %></td>
    <% if can? :update, article %>
        <td><%= link_to 'Edit', edit_article_path(article) %></td>
    <% end %>
    <% if can? :delete, article %>
        <td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td>
    <% end %>
  </tr>
<% end %>
</table>

All working now. Thanks for the help here and hopefully this will help someone else out if they run into this problem.

Jason