Search code examples
ruby-on-railsrubydeviseaccess-controlcancancan

Devise and Cancancan - How to make it work?


I am making a web app (chat sort of thing) since yesterday I switched from Pundit (as it was too difficult) to Cancancan (it looked better for me).

I am trying to make something simple to work such as displaying all Articles and its option (show, edit, destroy) and then setting permission on it so the only user that created such article will be able to edit or destroy it.

The problem is that I don't understand how it meant to be implemented fully. Google is lacking in examples and examples that are there are mostly outdated.

Here is what I have:

Ability.rb - I have no idea if this is even correct

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
        can :manage, :all
    else
        can :read, :all
    end

    can :read, :articles
    can :create, :articles
  end
end

User.rb (Devise)

class User
  include Mongoid::Document
  has_many :articles
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  ## Database authenticatable
  field :username,               type: String, default: ""
  field :email,              type: String, default: ""
  field :encrypted_password, type: String, default: ""

  ## Recoverable
  field :reset_password_token,   type: String
  field :reset_password_sent_at, type: Time

  ## Rememberable
  field :remember_created_at, type: Time

  ## Trackable
  field :sign_in_count,      type: Integer, default: 0
  field :current_sign_in_at, type: Time
  field :last_sign_in_at,    type: Time
  field :current_sign_in_ip, type: String
  field :last_sign_in_ip,    type: String

  ## Admin
  field :admin, :type => Boolean, :default => false
end

Article.rb

class Article
  include Mongoid::Document
  belongs_to :user

  field :title, type: String
  field :content, type: String

  default_scope -> { order(created_at: :desc) }
end

index.html (displaying articles - only part where I added Cancancan)

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

Solution

  • You need to define your authority by class in your Ability file:

    #app/models/ability.rb
    class Ability
      include CanCan::Ability
    
      def initialize(user)
        user ||= User.new # guest user (not logged in)
        if user.admin?
            can :manage, :all
        else
            can :read, :all
        end
    
        can [:credit, :edit, :update, :destroy], Article, user_id: user.id
      end
    end
    

    --

    #app/views/articles/index.html.erb
    <tbody>
       <% @articles.each do |article| %>
         <tr>
           <td><%= article.title %></td>
           <td><%= article.content %></td>
           <td><%= link_to 'Show', article %></td>
           <td><%= link_to 'Edit', article if can? :update, article %></td>
           <td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } if can? :destroy, article %></td>
          </tr>
        <% end %>
    </tbody>
    

    As an aside, the second important factor to consider with this is that Devise = authentication; CanCanCan = authorization:

    • Authentication = is user logged in?
    • Authorization = can user do this?

    I see a lot of people posting about "authorizing" with Devise, when it's completely false. Devise only handles authentication (user logged in?); when dealing with authorization, you need to work with a different pattern, harnessing the user object Devise created.

    Just wanted to point that out, considering you mentioned Devise in your original post.