3

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>
Mike Szyndel
  • 10,461
  • 10
  • 47
  • 63
Fresz
  • 1,804
  • 2
  • 16
  • 29
  • What you got here so far looks good to me. When logged in as an admin you should be able to see edit and destroy links, right? – Mike Szyndel Jan 23 '16 at 11:31
  • I have no way of testing admin (none of the accounts are actually admin ^^ ) How would I make the author of the article be able to edit and destroy it? – Fresz Jan 23 '16 at 11:45
  • Ok, I checked that admin functions work and they do. Now - how do I get the owner of the article to only be able to edit it? – Fresz Jan 23 '16 at 11:58
  • In your ability.rb add `can :manage, Article { |article| article.user == user }` – Mike Szyndel Jan 23 '16 at 12:02
  • This works because you pass `can? :update, @article` article object in you view to cancan! – Mike Szyndel Jan 23 '16 at 12:03
  • `can :manage, Article { |article| article.user == user }` this didnt work - `undefined method `Article' for #` – Fresz Jan 23 '16 at 12:07
  • You need to define the `class` in your `Ability` – Richard Peck Jan 23 '16 at 12:47

1 Answers1

5

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.

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • No problem, if you have any further questions, please don't hesitate to ask – Richard Peck Jan 23 '16 at 13:26
  • 1
    Don't worry I will ask - this is for my FYP and I meant to use technology that is new to me... So Ruby on Rails and MongoDB - in both cases I know not much. – Fresz Jan 23 '16 at 13:29