Search code examples
ruby-on-railscancan

Rails authorization with cancan


I am following this tutorial I am trying to authorize user only If user is admin he should be able to see all post and comments otherwise the normal user can see its own post only .I have read github page but was quite confusing


[post_controller.rb]

class PostsController < ApplicationController
    before_action :authenticate_user!, except: [:index, :show]

    def index
        @posts = Post.all.order('created_at DESC')
    end

    def new
        @post = Post.new
    end

    def show
        @post = Post.find(params[:id])
    end

    def create
        @post = Post.new(post_params)
        @post.user = current_user

        if @post.save
            redirect_to @post
        else
            render 'new'
        end
    end

    def edit
        @post = Post.find(params[:id])
    end

    def update
        @post = Post.find(params[:id])

        if @post.update(params[:post].permit(:title, :body))
            redirect_to @post
        else
            render 'edit'
        end
    end

    def destroy
        @post = Post.find(params[:id])
        @post.destroy

        redirect_to posts_path
    end

    private

    def post_params
        params.require(:post).permit(:title, :body)
    end
end


[comments_controller]

class CommentsController < ApplicationController
    def create
        @post = Post.find(params[:post_id])
        @comment = @post.comments.create(params[:comment].permit(:name, :body))
              @comment.user = current_user

   redirect_to post_path(@post)
    end

    def destroy
        @post = Post.find(params[:post_id])
        @comment = @post.comments.find(params[:id])
        @comment.destroy

        redirect_to post_path(@post)
    end
end


[ability.rb]

    class Ability


      include CanCan::Ability
   def initialize(user)
        unless user
        else
          case user.roles
          when 'admin'
            can :manage, Post
            can :manage, Comment
          when 'user' # or whatever role you assigned to a normal logged in user
            can :manage, Post, user_id: user.id
            can :manage, Comment, user_id: user.id
          end

   end


[comment.rb]

class Comment < ActiveRecord::Base
  belongs_to :post
end


[post.rb]

class Post < ActiveRecord::Base
    has_many :comments, dependent: :destroy
    validates :title, presence: true, length: {minimum: 5}
    validates :body,  presence: true
end


[user.rb]

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end


[migration]

class DeviseCreateUsers < ActiveRecord::Migration
  def change
    create_table(:users) do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip
      t.timestamps
    end
    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
   end
end


[migration]

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.string :name
      t.text :body
      t.references :post, index: true

      t.timestamps
    end
  end
end


[migration]

class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

Solution

  • It seems you do not yet have a user relationship to post and comment in which you need in order to identify if the user owns/created the comment/post

    Run:

    rails generate migration AddUserToPost user:belongs_to
    rails generate migration AddUserToComment user:belongs_to
    bundle exec rake db:migrate
    

    Then add the association relationships:

    post.rb

    class Post < ActiveRecord::Base
      belongs_to :user
      # ..
    end
    

    comment.rb

    class Comment < ActiveRecord::Base
      belongs_to :user
      # ..
    end
    

    user.rb

    class User < ActiveRecord::Base
      has_many :posts
      has_many :comments
      # ..
    end
    

    Now you can identify who owns the post/comment, and what posts/comments a user owned/created with something like the following pseudo-code:

    # rails console
    post = Post.find(1)
    post_owner = post.user
    
    comment = Comment.find(1)
    comment_owner = comment.user
    
    user = User.find(1)
    user_comments = user.comments
    user_posts = user.posts
    

    Now, the next step is to auto-associate the logged-in user to newly created posts/comments. This is done through the controllers:

    posts_controller.rb

    class PostsController < ApplicationController
      authorize_resource
      # ..
    
      def create
        @post = Post.new(post_params)
        @post.user = current_user # I assume you have a variable current_user, or if you are using Devise current_user is already accessible
    
        if @post.save
          redirect_to @post
        else
          render :new
        end
      end
    end
    

    comments_controller.rb

    class CommentsController < Application
      authorize_resource
      # ..
    
      def create
        @post = Post.find(params[:post_id])
        @comment = @post.comments.build(params[:comment].permit(:name, :body))
            #puts "hhhhhhhhhh#{@comment}"
        @comment.user = current_user # I assume you have a variable current_user, or if you are using Devise current_user is already accessible
    
        @comment.save
    
        redirect_to post_path(@post)
      end 
    end
    

    Now, at this point. Whenever a post/comment gets created, the logged-in user is automatically associated to it (as the owner).

    Finally, we could just update the Ability class to only authorize users to :edit, :update, :show, and :destroy actions, if the user_id: current_user (logged-in user).

    ability.rb

    class Ability
      include CanCan::Ability
    
      def initialize(user)
        # if not logged in (Guest)
        unless user
          # cant do anything unless you add more `can` here
        # else if logged in
        else
          case user.role
          when 'admin'
            can :manage, Post
            can :manage, Comment
          when 'normal' # or whatever role you assigned to a normal logged in user
            can :manage, Post, user_id: user.id
            can :manage, Comment, user_id: user.id
          # If you don't have a role name for a normal user, then use the else condition like Rich Peck's answer. Uncomment the following instead, and then comment the `when 'normal' block of code just above
          # else
          #   can :manage, Post, user_id: user.id
          #   can :manage, Comment, user_id: user.id
          end
        end
      end
    end
    

    Just a final helpful information to the Ability above:

    • can :manage, Post, user_id: user.id

      This is just a shorthand equal to:

      can [:show, :edit, :update, :destroy], Post, user_id: user.id
      can [:index, :new, :create], Post
      

      You will notice that user_id: user.id is not taken into consideration for :index, :new, and :create because these are :collection methods, and not :member methods. More info here

    • If you want readability and customizability, you may opt to use the longer one above instead of the shorthand :manage.