Rails 4 - Pundit with Rolify - permitting a group of roles
Asked Answered
T

3

6

I am trying to make an app with Rails 4.

I have defined a series of roles with Rolify gem.

Now, I want to use pundit to allow users with a role to do certain things. Where more than one type of role can do a thing, I have defined a group of roles.

In my application_policy, I have defined private methods which set out the groups of roles that I want to use in the pundit permissions.

My application policy instantiates user and record. I then define record as the name of the relevant model (the same name as the policy for that model).

I have:

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    scope.where(:id => record.id).exists?
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope
    end
  end

  private


    def cf_legal
      [ :Admin, :CF_Legal, :CF_Policy_Manager ]
    end

    def cf_content
      [ :Admin, :CF_Author, :CF_Editor ]
    end


end

Then in my content policy, I want to say:

def record
   content
end

def create
    user.has_role? :cf_content
end

When I save this and try it, I can't see the thing that I am supposed to see (as a user with the role Author.

Can anyone see how to do this?

Trin answered 21/2, 2016 at 3:33 Comment(0)
O
2

tl;dr: Use query methods on your Policy class.

First, the model should have it's own Policy class that (optionally) extends your ApplicationPolicy class. Let's say your model is called Post. Then you could do something like:

class PostPolicy < ApplicationPolicy

   attr_reader :user, :post

   def initialize(user,post)
     @user = user
     @post = post
   end

   def create?
     cf_content.all? { |role| user.has_role?(role) }
   end

   private

   def cf_content
    [ :Admin, :Author, :Editor ]
   end
end


class PostsController
  def create
    @post = Post.new(params[:post])
    authorize @post, :create?
    # @post.save and redirect, etc.
  end
end

And the authorize call will invoke the create? query method and check if the user has the roles in cf_content.

You may not even need to add the second argument, create?, as the pundit documentation says:

The authorize method automatically infers that Post will have a matching PostPolicy class, and instantiates this class, handing in the current user and the given record. It then infers from the action name, that it should call update? on this instance of the policy.

Or in your case, create? instead of update?.

Okwu answered 29/2, 2016 at 0:0 Comment(1)
Hi Josh, I dont understand any of what you've written. I'll edit my post to clarify my question.Trin
H
2

You can check for multiple roles like this :

user.admin? or user.author?

If you want to check for role from an array you can also check with : cf_content.include?(user.role) OR cf_content.include?(user.role.title) whichever suits the scenairio.

You can also check for instance bound roles with -> user.applied_roles and see if the roles it returns includes your expected role.

Housum answered 6/3, 2016 at 3:18 Comment(0)
I
1

I think you need to use

has_any_role?

Which takes two or more args passed as symbols. But your cf_content method is returning an array. If the user needs the 3 roles you've defined in cf_create You need to do something more like

def create
  cf_content.all? { |role| user.has_role?(role) }
end

UPDATE:

If you only need one role than simple change:

def create
    cf_content.any? { |role| user.has_role?(role) }
end

Also, I'm not sure what these represent, but if they are the names of your roles, I would suggest using lower case. So instead of

[ :Admin, :CF_Author, :CF_Editor ]

You might use:

[ :admin, :cf_author, :cf_editor ]

UPDATE 2:

The has_role? will not accept an array. So if you want to check has_role? on an array you need to iterate has_role? over each item in the array. Since :cf_content method returns and array of roles, then change from:

def create
    user.has_role? :cf_content
end

To:

def create
   :cf_content.any? { |role| user.has_role?(role) }
end

But you have not explained where you are trying to put your "content policy".

Idiocrasy answered 1/3, 2016 at 7:30 Comment(3)
The user doesnt need three roles, it can have any one of the roles in the array. The roles work in other parts of the app in title case, so unless there is a pundit specific reason to rename everything to lower case, then I'll keep that as it is. Not sure about the show method, it works fine as I have it. My problem is how to get an array of roles to pass as true to pundit if the user has any of the roles in the array.Trin
See my updated answer. As to case of role names, I was recommending using ruby / rails naming conventions. Generally only class or module names use upper case so I was confused by them and even stack overflow syntax highlighted :Admin as if it's a class name. See github.com/bbatsov/ruby-style-guide#namingIdiocrasy
Hi - your answer still doesnt make sense to me (sorry if I'm missing the point). My show action works fine - that's not what I'm asking for help with. I want to know how to define an array of roles, so that if the current user has any of the roles in the array - then the pundit action will be permitted. Do you know how to fix my code to make that happen?Trin

© 2022 - 2024 — McMap. All rights reserved.