Question

So I decided to give a try to pundit user authorization solution. I wonder how to use the policy helper in view where an instance variable might be nil, as in the simple case below:

app/views/projects/index.html.slim

h1 Projects
(...)
- if policy(@project).create?
  = link_to 'New Project', new_project_path

app/controllers/projects_controller.rb

(...)
def index
  @projects = Project.all
end
(...)

app/policies/project_policy.rb

class ProjectPolicy < Struct.new(:user, :project)
  def create?
    user.has_role? :admin
  end

I want to show a "New Project" link on Projects#index page, but I do not have a @project instance variable available in this view, getting an error:

Pundit::NotDefinedError in Projects#index

unable to find policy NilClassPolicy for

The error appears apparently because I do pass @project instance variable that is nil, therefore has a NilClass which apparently I do not have a need to authorize.

I found 2 workarounds to this problem that make it run correctly, but none of them seems to be appropriate:

  1. Make use of existing @projects variable in the view, i.e.: policy(@projects[0])
  2. add a line to projects#index controller action that defines this instance variable, ex. @project = Project.new (or directly in view similar to above: policy(Project.new))

First solution will cause same error in @projects array would be empty, while second one creates redundant instance variable. All policy helper needs to know is on which class I do want to enforce the authorisation logic.

Any suggestions on proper way to achieve it?

Was it helpful?

Solution

Your second proposed solution is what I do.

- if policy(@project).create?
  = link_to 'New Project', new_project_path

The policy check here for a new record should use Project.new (whether it's assigned to an instance variable or not).

- if policy(Project.new).create?
  = link_to 'New Project', new_project_path

One way or another, an instance of Project must be passed to the policy helper in order for Pundit to derive the policy class ProjectPolicy to do the create? check lookup on. When you pass in nil it's why you're seeing Pundit derive a NilClassPolicy.

OTHER TIPS

Usually individual instances have the same policy as all other instances in the class - this is in fact the normal way Pundit works. In this case, you really don't care about the policy that belongs to a specific instance; instead, you're looking for the policy of a class of objects.

The policy method uses this find method to identify the policy class for your object.

def find
  if object.respond_to?(:policy_class)
    object.policy_class
  elsif object.class.respond_to?(:policy_class)
    object.class.policy_class
  else
    klass = if object.respond_to?(:model_name)
      object.model_name
    elsif object.class.respond_to?(:model_name)
      object.class.model_name
    elsif object.is_a?(Class)
      object
    else
      object.class
    end
    "#{klass}Policy"
  end
end

You can pass any object to the policy method. Since classes are objects, you can pass the class in. In the find method, the first condition checks whether the object you passed responds to policy_class, so you can define a method in your Project class called policy_class and have that method return your ProjectPolicy class.

class Project < ActiveRecord::Base
  def self.policy_class
    ProjectPolicy
  end
end

If you don't define the policy_class method, then the first two conditions will fail and the find method will then try to construct a policy class name. It first looks at object.model_name. If you're in Rails and your model extends ActiveModel::Naming (which ActiveRecord::Base does), then it will already respond to model_name, and if you're doing typical Rails-ey stuff, then that name is exactly what you want: it will return "Project". The find method will then affix "Policy" to the end of that to make "ProjectPolicy". If it is some other object, the name of the object is used, and that would work for most cases as well.

Alternatively, you can pass any prototype object that represents the type of object for which you want the policy. That means you can use Policy.new to create one, or you can grab one out of your @policies array. As you said, those options have drawbacks.

There are lots of options available to you. There isn't a particularly "correct" way. I prefer passing the class in a case like this as it makes the most semantic sense.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top