Rails 4 - Pundit - scoped policy for index - ruby-on-rails

I am trying to learn how to use Pundit with my Rails 4 app.
I have the following models:
class User < ActiveRecord::Base
has_one :profile
has_many :eois
end
class Profile < ActiveRecord::Base
belongs_to :user
has_many :projects, dependent: :destroy
end
class Project < ActiveRecord::Base
belongs_to :profile
has_many :eois
end
class Eoi < ActiveRecord::Base
belongs_to :project
belongs_to :user
end
I have a scoped EoiPolicy with:
class EoiPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.profile.project.id == #eoi.project_id?
scope.where(project_id: #user.profile.project.id)
elsif user.id == eoi.user_id?
scope.where(user_id: user.id)
else
nil
end
end
end
def index?
user.profile.project.id == #eoi.project_id? or user.id == eoi.user_id?
end
def new?
true
end
def show?
user.profile.project.id == #eoi.project_id? or user.id == eoi.user_id?
end
def edit?
user.id == eoi.user.id?
end
def create?
true
end
def update?
user.id == eoi.user.id?
end
def destroy?
user.id == eoi.user.id?
end
end
In my EoisController, I have tried to use the scope with:
def index
# #eois = #project.eois
#eois = policy_scope(Eoi)
# #eois = Eois.find_by_project_id(params[:project_id])
end
Then in my view/eois/index, I have tried to display the index with:
<% policy_scope(#user.eois).each do |group| %>
I can't get this to work. The error message highlights this line of my scope method in the policy:
if user.profile.project.id == #eoi.project_id?
To me, this looks correct, although I'm still trying to figure this out. Can anyone see what needs to happen to make this work, so that if the user is the user, who's profile owns the relevant project, all eois relating to that project are visible.
Otherwise, if the user is the user who created the eoi, then all eois they have created are visible?
The error message says:
undefined method `project' for #<Profile:0x007fa03f3faf48>
Did you mean? projects
projects=
I'm wondering if that's because an index will have many records, it needs to show something different in the policy to recognise the plurality?
I have also tried replacing that line with:
if #eoi.project_id == #user.profile.project.id?
although that is also wrong and gives
undefined method `project_id' for nil:NilClass
Did you mean? object_id
I also tried making the scope:
def resolve
# cant figure what is wrong with this
if eoi.project_id == user.profile.project.id?
scope.where(project_id: #user.profile.project.id)
else
nil
end
end
but that's also wrong and gives this error:
undefined local variable or method `eoi' for #<EoiPolicy::Scope:0x007ffb505784f8>
I also tried:
def resolve
# cant figure what is wrong with this
if #eoi.project_id == user.profile.project.id? or Eoi.project_id == user.profile.project.id?
scope.where(project_id: #user.profile.project.id)
elsif user.id == eoi.user_id?
scope.where(user_id: user.id)
else
nil
end
end
end
def index?
user.profile.project.id == Eoi.project_id? or user.id == Eoi.user_id?
end
but that attempt gives this error message:
undefined method `project_id' for nil:NilClass
Did you mean? object_id
CURRENT THOUGHT
I think I need to pass more than user and scope to the scope method. If I can also pass project, then I can make the scope referable to the project to which the EoI relates.
If I could have this working, then maybe I could get the scope method to work for the index view on the controller:
class Scope
attr_reader :user, :scope
def initialize(user, scope, project)
#user = user
#scope = scope
#project = project
end
end
then in the controller:
def index
# #eois = #project.eois
#eois = policy_scope(Eoi, #project)
# authorize #eois
# #eois = Eois.find_by_project_id(params[:project_id])
end
This doesnt work, when I try I get an error saying that the policy
wrong number of arguments (given 2, expected 1)
Please help!
NEXT ATTEMPT
My next attempt is to try taking the suggestions from [this]Pundit issue and implement that idea for how to get the right scope for a particular user.
In my Eoi Policy, I changed the resolve method to:
class Scope
attr_reader :user, :scope
def initialize(user, scope) #project
#user = user
#scope = scope
# #project = project
end
def resolve
# if Eoi.project_id == user.profile.project.id? or Eoi.project_id == user.profile.project.id?
if user.id == eoi.projects.profile.user.map(&:id)
scope.joins(eois: :projects).where(project_id: user.profile.projects.map(&:id)).empty?
# if scope.eoi.project_id == user.profile.projects.map(&:id)
# scope.where(project_id: user.profile.projects.map(&:id)).empty?
# scope.where(project_id: user.profile.project.id)
# elsif user.id == eoi.user_id?
# scope.where(user_id: user.id)
else
# nil
end
end
end
Then in my eoi controller index action, I tried this:
def index
# #eois = #project.eois
# #eois = policy_scope(Eoi, #project)
policy_scope(Eoi).where(project_id: params[:project_id])
# authorize #eois
# #eois = Eois.find_by_project_id(params[:project_id])
end
That doesnt work either. The error message for this attempt says:
undefined local variable or method `eoi' for #<EoiPolicy::Scope:0x007f98677c9cf8>
Im out of ideas for things to try. Can anyone see a way to give the scope the right inputs to set this up?
OBSERVATION
I have noticed that a lot of the repos on github that use Pundit with scopes also include a method like this:
def scope
Pundit.policy_scope!(user, record.class)
end
That method is in addition to the Scope class and isn't shown in the Pundit gem docs. If that is necessary to include, what does it do?
1
REWRITE
I've now looked through more than 200 repos on github for insight into how I'm supposed to write a policy to meet my objectives. I'm out of ideas for how to use Pundit as intended.
I've changed my setup completely to try and work around the bits I can't understand. I now have:
Eois Controller
class EoisController < ApplicationController
def index
#eois = Eoi.by_user_id(current_user.id)
end
end
Projects:: Eois controller
module Projects
class EoisController < ApplicationController
before_action :get_project
before_action :set_eoi, only: [:edit, :update, :destroy]
# after_action :verify_authorized
def index
#eois = Project.by_user_id(current_user.id).find_by(id: params[:project_id]).try(:eois) || []
end
def show
#eoi = Eoi.find(params[:id])
authorize #eoi
end
def set_eoi
#eoi = EoiPolicy::Scope.new(current_user, params[:project_id]).resolve.find(params[:id])
end
def get_project
#project = Project.find(params[:project_id])
end
Eoi Policy (to decide when to show all eois made by a user)
class EoiPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if scope.present?
Eoi.by_user_id(user.id)
# end
else
[]
end
end
end
def index?
user.profile.project.id == Eoi.project_id? or user.id == Eoi.user_id?
end
def new?
true
end
def show?
record.user_id == user.id || user.profile.project_id == record.project_id
# user.profile.project.id == #eoi.project_id? or user.id == eoi.user_id?
end
def edit?
user.id == eoi.user.id?
end
def create?
true
end
def update?
user.id == eoi.user.id?
end
def destroy?
user.id == eoi.user.id?
end
end
Routes
resources :eois
resources :projects do
member do
resources :eois, controller: 'projects/eois
end
When I want to show EoIs that are submitted in relation to a project, I use the Projects Eoi Policy and when I want to show the Eois that a user has created, I use the Eoi Policy -- no scopes.
I would love to figure this out so I can use this gem the way it is intended. Advice would be greatly appreciated. I'm sure this attempt isn't what Pundit is meant for - but I can't figure out how to use this gem as shown in the docs.
I can't use policy_scope because I need to pass the project_id param into the index action for the projects eoi controller index action.
PaReeOhNos SUGGESTION
My attempt at trying to implement PareeOhNos suggestion is set out below. I'm not sure I understand it properly because eois will always have a project id and a user id, but maybe I'm not getting the point of what the load_parent method is doing.
In my Eois Controller, I have:
class EoisController < ApplicationController
before_action :load_parent
before_action :load_eoi, only: [:show, :edit, :update, :destroy]
def index
authorize #parent
#eois = EoiPolicy::Scope.new(current_user, #parent).resolve
end
def show
end
# GET /eois/new
def new
#project = Project.find(params[:project_id])
#eoi = #project.eois.build
#contribute = params[:contribute] || false
#participate = params[:participate] || false
#partner = params[:partner] || false
#grant = params[:grant] || false
#invest = params[:invest] || false
end
# GET /eois/1/edit
def edit
end
# POST /eois
# POST /eois.json
def create
#eoi = Project.find(params[:project_id]).eois.build(eoi_params)
#eoi.user_id = #current_user.id
respond_to do |format|
if #eoi.save
format.html { redirect_to Project.find(params[:project_id]), notice: 'Eoi was successfully created.' }
format.json { render :show, status: :created, location: #project }
else
format.html { render :new }
format.json { render json: #eoi.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /eois/1
# PATCH/PUT /eois/1.json
def update
respond_to do |format|
if #eoi.update(eoi_params)
format.html { redirect_to #project, notice: 'Eoi was successfully updated.' }
format.json { render :show, status: :ok, location: #eoi }
else
format.html { render :edit }
format.json { render json: #eoi.errors, status: :unprocessable_entity }
end
end
end
# DELETE /eois/1
# DELETE /eois/1.json
def destroy
#eoi.destroy
respond_to do |format|
format.html { redirect_to #project, notice: 'Eoi was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def load_parent
# #parent = (params[:project_id] ? Project.find(params[:project_id] : current_user)
#parent = params[:project_id] ? Project.find(params[:project_id]) : current_user
end
def load_eoi
#eoi = Eoi.find(params[:id])
authorize #eoi
end
In my Eoi Policy, I have:
class EoiPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if scope.is_a?(User)
Eoi.where(user_id: scope.id)
elsif scope.is_a?(Project)
Eoi.where(project_id: scope.id)
else
[]
end
end
end
def index?
record.is_a?(User) || user.profile.project.id == record.project_id
end
def new?
true
end
def show?
record.user_id == user.id || user.profile.project_id == record.project_id
end
def edit?
user.id == eoi.user.id?
end
def create?
true
end
def update?
user.id == eoi.user.id?
end
def destroy?
user.id == eoi.user.id?
end
end
In my routes.rb, I have:
resources :projects do
member do
resources :eois, shallow: true
resources :eois, only: [:index]
In my eois/index, I have:
<% #eois.sort_by(&:created_at).in_groups_of(2) do |group| %>
<% group.compact.each do |eoi| %>
<h4><%= link_to eoi.user.full_name %></h4>
<%= link_to 'VIEW DETAILS', eoi_path(eoi), :class=>"portfolio-item-view" %>
<% end %>
<% end %>
In my eois/ show, I have:
"test"
When I try all this, the eois/index page loads. When I try to show a specific eoi page, I get an error that says:
wrong number of arguments (given 2, expected 0)
the error message points to authorise #eoi line of the controller:
def load_eoi
#eoi = Eoi.find(params[:id])
authorize #eoi
end
The same error arises if I put authorize #eoi in the show action instead of the load eoi method.
APPLICATION POLICY HAS
class ApplicationPolicy
attr_reader :user, :scope
class Scope
def initialize(user, scope)
#byebug
#user = user
# record = record
#scope = scope
end
def resolve
scope
end
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
NEXT ATTEMPT
Taking PaReeOhNos suggestion (copied above), I've tried to adapt it a bit to better fit my use cases.
Now, I have:
Eoi controller
class EoisController < ApplicationController
# before_action :get_project
# before_action :set_eoi, only: [:show, :edit, :update, :destroy]
before_action :load_parent
before_action :load_eoi, only: [:show, :edit, :update, :destroy]
# GET /eois
# GET /eois.json
# def index
# #eois = #project.eois
# # #eois = Eois.find_by_project_id(params[:project_id])
# end
def index
# authorize #parent
#eois = policy_scope(Eoi.where(project_id: params[:project_id]))
# #eois = EoiPolicy::Scope.new(current_user, #parent).resolve
end
# GET /eois/1
# GET /eois/1.json
def show
end
# GET /eois/new
def new
#project = Project.find(params[:project_id])
#eoi = #project.eois.build
#contribute = params[:contribute] || false
#participate = params[:participate] || false
#partner = params[:partner] || false
#grant = params[:grant] || false
#invest = params[:invest] || false
end
# GET /eois/1/edit
def edit
end
# POST /eois
# POST /eois.json
def create
#eoi = Project.find(params[:project_id]).eois.build(eoi_params)
#eoi.user_id = #current_user.id
respond_to do |format|
if #eoi.save
format.html { redirect_to Project.find(params[:project_id]), notice: 'Eoi was successfully created.' }
format.json { render :show, status: :created, location: #project }
else
format.html { render :new }
format.json { render json: #eoi.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /eois/1
# PATCH/PUT /eois/1.json
def update
respond_to do |format|
if #eoi.update(eoi_params)
format.html { redirect_to #project, notice: 'Eoi was successfully updated.' }
format.json { render :show, status: :ok, location: #eoi }
else
format.html { render :edit }
format.json { render json: #eoi.errors, status: :unprocessable_entity }
end
end
end
# DELETE /eois/1
# DELETE /eois/1.json
def destroy
#eoi.destroy
respond_to do |format|
format.html { redirect_to #project, notice: 'Eoi was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def load_parent
# #parent = (params[:project_id] ? Project.find(params[:project_id] : current_user)
#parent = params[:project_id] ? Project.find(params[:project_id]) : current_user
end
def load_eoi
#eoi = Eoi.find(params[:id])
# authorize #eoi
end
Eoi policy
class EoiPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
# since we send the scoped eois from controller, we can pick
# any eoi and get its project id
# check if the current user is the owner of the project
# if (user.profile.projects.map(&:id).include?(project_id))
# # user is the owner of the project, get all the eois
# scope.all
# end
# #not the owner , then get only the eois created by the user
# scope.where(user_id: user.id)
# end
if scope.is_a?(User)
Eoi.where(user_id: scope.id)
elsif scope.is_a?(Project) && (user.profile.projects.map(&:id).include?(project_id))
project_id = scope.first.project_id
Eoi.where(project_id: scope.id)
else
Eoi.none
end
end
end
def index?
record.is_a?(User) || user.profile.project.id == record.project_id
end
def new?
true
end
def show?
record.user_id == user.id || user.profile.project_id == record.project_id
end
def edit?
user.id == eoi.user.id?
end
def create?
true
end
def update?
user.id == eoi.user.id?
end
def destroy?
user.id == eoi.user.id?
end
end
Routes
resources :eois#, only: [:index]
concern :eoiable do
resources :eois
end
resources :projects do
concerns :eoiable
end
Index
<% #eois.sort_by(&:created_at).in_groups_of(2) do |group| %>
<% group.compact.each do |eoi| %>
<h4><%= link_to eoi.user.full_name %></h4>
<%= link_to 'VIEW DETAILS', project_eoi_path(eoi.project, eoi), :class=>"portfolio-item-view" %>
<% end %>
<% end %>
View
'test'
This isn't working, because when I navigate to a project and then try to render the index of eois that have a matching project id, I get an empty index page, when I have 4 records in my database that should be rendered.
LEITO'S SUGGESTION
Taking Leito's suggestion, I've also tried this:
Eoi Controller
class EoisController < ApplicationController
before_action :get_project
before_action :set_eoi, only: [:show, :edit, :update, :destroy]
# before_action :load_parent
# before_action :load_eoi, only: [:show, :edit, :update, :destroy]
# GET /eois
# GET /eois.json
# def index
# #eois = #project.eois
# # #eois = Eois.find_by_project_id(params[:project_id])
# end
def index
# authorize #eois
# authorize #parent
# policy_scope(#project.eois)
#eois = policy_scope(Eoi.where(project_id: params[:project_id]))
# #eois = EoiPolicy::Scope.new(current_user, #parent).resolve
end
# GET /eois/1
# GET /eois/1.json
def show
end
# GET /eois/new
def new
#project = Project.find(params[:project_id])
#eoi = #project.eois.build
#contribute = params[:contribute] || false
#participate = params[:participate] || false
#partner = params[:partner] || false
#grant = params[:grant] || false
#invest = params[:invest] || false
end
# GET /eois/1/edit
def edit
end
# POST /eois
# POST /eois.json
def create
#eoi = Project.find(params[:project_id]).eois.build(eoi_params)
#eoi.user_id = #current_user.id
respond_to do |format|
if #eoi.save
format.html { redirect_to Project.find(params[:project_id]), notice: 'Eoi was successfully created.' }
format.json { render :show, status: :created, location: #project }
else
format.html { render :new }
format.json { render json: #eoi.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /eois/1
# PATCH/PUT /eois/1.json
def update
respond_to do |format|
if #eoi.update(eoi_params)
format.html { redirect_to #project, notice: 'Eoi was successfully updated.' }
format.json { render :show, status: :ok, location: #eoi }
else
format.html { render :edit }
format.json { render json: #eoi.errors, status: :unprocessable_entity }
end
end
end
# DELETE /eois/1
# DELETE /eois/1.json
def destroy
#eoi.destroy
respond_to do |format|
format.html { redirect_to #project, notice: 'Eoi was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# def load_parent
# # #parent = (params[:project_id] ? Project.find(params[:project_id] : current_user)
# #parent = params[:project_id] ? Project.find(params[:project_id]) : current_user
# end
# def load_eoi
# #eoi = Eoi.find(params[:id])
# # authorize #eoi
# end
# # Use callbacks to share common setup or constraints between actions.
def set_eoi
#eoi = Eoi.find(params[:id])
end
def get_project
#project = Project.find(params[:project_id])
end
Eoi Policy
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if scope.joins(project: :profile).where profiles: { user_id: user }
Eoi.where(project_id: scope.ids)
elsif scope.joins(eoi: :user).where eois: { user_id: user }
Eoi.where(user_id: scope.ids)
else
Eoi.none
end
# since we send the scoped eois from controller, we can pick
# any eoi and get its project id
# check if the current user is the owner of the project
# if (user.profile.projects.map(&:id).include?(project_id))
# # user is the owner of the project, get all the eois
# scope.all
# end
# #not the owner , then get only the eois created by the user
# scope.where(user_id: user.id)
# end
# if scope.is_a?(User)
# Eoi.where(user_id: scope.id)
# elsif scope.is_a?(Project) && (user.profile.projects.map(&:id).include?(project_id))
# project_id = scope.first.project_id
# Eoi.where(project_id: scope.id)
# else
# Eoi.none
# end
end
end
def index?
true
# record.is_a?(User) || user.profile.project.id == record.project_id
end
def new?
true
end
def show?
true
# record.user_id == user.id || user.profile.project_id == record.project_id
end
def edit?
user.id == eoi.user.id?
end
def create?
true
end
def update?
user.id == eoi.user.id?
end
def destroy?
user.id == eoi.user.id?
end
end
The routes and views are the same as the attempt above
The problem here is with the get project method in my controller. I need that for the scenario where Im trying to show all the eois on a specific project. I don't need it when I'm trying to show all of a user's eois.
When I save all this and try it, the eois on a project show correctly. However the eois (not nested inside a project) that are supposed to show me all of my (as a user) eois, shows an error that says:
Couldn't find Project with 'id'=
The error message highlights the 'get_project method'.
LEITO'S UPDATED SUGGESTION
Taking Leito's updated suggestion, I have set out the current attempt.
Before doing so, I want to clarify that all Eois will have both a user id and a project id. I use this table for users to express interest in projects. My objective is to have the user whose profile owns the project to see all eois submitted on that project. Then, I also want users to see all of their own eois submitted (across all projects).
Eoi Policy
def resolve
if scope.joins(project: :profile).where 'profiles.user_id = ? OR eois.user_id = ?', user.id, user.id
Eoi.all
else
Eoi.none
end
Eoi controller
def index
#eois = policy_scope(Eoi)
#eois = #eois.where(project_id: params[:project_id]) if params[:project_id]
end
Currently this works fine in finding the eois that are nested under a project (project/26/eois). However, when I try to do eois/index (not nested under project), which I want to return all the user's eois, I get an error that says:
Couldn't find Project with 'id'=
It highlights this line of the eoi controller:
def get_project
#project = Project.find(params[:project_id])
end
I'm not sure I understand the resolve method or the controller culling idea now. I can't see what's wrong with the scope line to see what to try changing.

I'm the previous commenter on that issue.
For your EoiScope, you simply want what Eois the user has access to (because they belong to projects under this profile), independent from the project (this requirement is only for the controller, because is nested), so your controller should look something like this:
Edit: Based on your latest attempt, I've updated the scope to account for Eois belonging directly to the user (not through a project) and you should simply scope it to a project or not based on the presence of params[:project_id], see updated answer.
#eois = policy_scope(Eoi)
#eois = #eios.where(project_id: params[:project_id]) if params[:project_id]
And your scope should do joins until it reaches user or simply look for the user_id property on Eoi.
class EoiPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.joins(project: : profile).where 'profiles.user_id = ? OR eois.user_id = ?', user.id, user.id
end
end
# Other methods that differ from ApplicationPolicy's methods
end
Please note, Scope isn't calling eoi, but default* scope only knows about scope and user. * By default, I mean when it inherits from ApplicationPolicy::Scope

In your first example, there's a couple of issues. Firstly, #eoi does not exist, and can't exist. The #eoi variable is set in the controller, and this is a different object. It doesn't work in the same way as your views where this is accessible, so this will never be set.
Equally, the eoi variable will not be set, as your initialize method is only assigning the user and resource variables, so they're the only two you have access to (unless you rename)
The scope in the policy works a little differently to how you think it works. The policy itself generally takes the user logged in, and a class, or a record that you are authorising. The scope however, doesn't normally take a record as the second argument. It is a scope, so either an active record sub-class, or a relation. You're not restricted to this however, and you could work around it by supplying a record but do note this is not normal behaviour for Pundit.
In order to achieve what you're after, you should only have to make a few adjustments:
class EoiPolicy < ApplicationPolicy
class Scope
attr_reader :user, :eoi
def initialize(user, eoi)
#user = user
#eoi = eoi
end
def resolve
if user.profile.project.id == eoi.project_id
Eoi.where(project_id: user.profile.project.id)
elsif user.id == eoi.user_id
Eoi.where(user_id: user.id)
else
nil
end
end
end
def index?
user.profile.project.id == record.project_id or user.id == record.user_id
end
def new?
true
end
def show?
user.profile.project.id == record.project_id? or user.id == record.user_id
end
def edit?
user.id == record.user.id
end
def create?
true
end
def update?
user.id == record.user.id
end
def destroy?
user.id == record.user.id
end
end
The main changes here are that the attr_reader :user, :scope is now attr_reader :user, :eoi which will give you access to eoi within that scope.
Access to this is no longer prefixed with # as this is in-line with how pundit works.
Throughout the rest of the policy, #eoi again cannot work, but this has been changed to record (assuming this is what it is in ApplicationPolicy). Please bear in mind the the Scope, and the rest of the policy are two different classes.
With this setup, you should now be able to simply call policy_scope(#eoi) from within your controller. Note the usage of the #eoi variable here and NOT the Eoi class as before. This is crucial, as without this, you won't have access to things like user_id or project_id as those methods don't exist in the Eoi class, but only a record.
I've also removed the ? symbols from the end of your if conditions. These are generally used to signify that the method being called returns a boolean, whereas you had them on the end of something that simply returns an integer. I'd imagine you'd actually get an error saying the method doesn't exist but if you've renamed things then you may want to put them back, but as I say that does go against ruby coding styles.
And on a side-note, using or or and in statements instead of || or && can on the odd occasion behave differently to how you expect. In most scenarios it's fine, but it doesn't technically mean the same thing.
Hope this all helps, let me know if you have any further issues with it.

For others, I'm not sure if this is a solution that makes use of Pundit in the way it was intended, however it does generate the flows that I want, within the limits of my ability.
Thank you to everyone who helped on this. I'm sure I've still got lots to learn about how to improve this, but for now, this is a solution that works.
In summary - I now have two policies for 1 controller.
Eoi Policy
class EoiPolicy < ApplicationPolicy
class Scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
# selects all the EOI's for a given user
#scope.where(user_id: #user.id)
end
end
def index?
true
end
Project Eoi Policy
class ProjectEoiPolicy < ApplicationPolicy
class Scope < Scope
def resolve(project_id)
project = Project.find(project_id)
if project.owner?(#user)
# if the user is the owner of the project, then get
# all the eois
project.eois
else
# select all the eois for the project
# created by this user
Eoi.for_user(#user.id).for_project(project_id)
end
end
end
end
Eoi Controller index action
class EoisController < ApplicationController
before_action :get_project, except: [:index, :show]
before_action :set_eoi, only: [:show, :edit, :update, :destroy]
def index
if params[:project_id]
#eois = ProjectEoiPolicy::Scope.new(current_user, Eoi).resolve(params[:project_id])
else
#eois = policy_scope(Eoi)
end
end

Related

What is the DRY way to restrict an entire controller with Pundit in Rails?

I'm using Pundit with Rails, and I have a controller that I need to completely restrict from a specific user role. My roles are "Staff" and "Consumer." The staff should have full access to the controller, but the consumers should have no access.
Is there a way to do this that is more DRY than restricting each action one-by-one?
For instance, here is my policy:
class MaterialPolicy < ApplicationPolicy
attr_reader :user, :material
def initialize(user, material)
#user = user
#material = material
end
def index?
user.staff?
end
def show?
index?
end
def new?
index?
end
def edit?
index?
end
def create?
index?
end
def update?
create?
end
def destroy?
update?
end
end
And my controller:
class MaterialsController < ApplicationController
before_action :set_material, only: [:show, :edit, :update, :destroy]
# GET /materials
def index
#materials = Material.all
authorize #materials
end
# GET /materials/1
def show
authorize #material
end
# GET /materials/new
def new
#material = Material.new
authorize #material
end
# GET /materials/1/edit
def edit
authorize #material
end
# POST /materials
def create
#material = Material.new(material_params)
authorize #material
respond_to do |format|
if #material.save
format.html { redirect_to #material, notice: 'Material was successfully created.' }
else
format.html { render :new }
end
end
end
# PATCH/PUT /materials/1
def update
authorize #material
respond_to do |format|
if #material.update(material_params)
format.html { redirect_to #material, notice: 'Material was successfully updated.' }
else
format.html { render :edit }
end
end
end
# DELETE /materials/1
def destroy
authorize #material
#material.destroy
respond_to do |format|
format.html { redirect_to materials_url, notice: 'Material was successfully destroyed.' }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_material
#material = Material.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def material_params
params.require(:material).permit(:name)
end
end
Is there a way to do this that I'm not understanding, or is that how Pundit is designed, to require you to be explicit?
The first step is just to move the call to authorize to your callback:
def set_material
#material = Material.find(params[:id])
authorize #material
end
You can also write #material = authorize Material.find(params[:id]) if your Pundit version is up to date (previous versions returned true/false instead of the record).
Pundit has a huge amount of flexibility in how you choose to use it. You could for example create a separate headless policy:
class StaffPolicy < ApplicationPolicy
# the second argument is just a symbol (:staff) and is not actually used
def initialize(user, symbol)
#user = user
end
def access?
user.staff?
end
end
And then use this in a callback to authorize the entire controller:
class MaterialsController < ApplicationController
before_action :authorize_staff
# ...
def authorize_staff
authorize :staff, :access?
end
end
Or you can just use inheritance or mixins to dry your policy class:
class StaffPolicy < ApplicationPolicy
%i[ show? index? new? create? edit? update? delete? ].each do |name|
define_method name do
user.staff?
end
end
end
class MaterialPolicy < StaffPolicy
# this is how you would add additional restraints in a subclass
def show?
super && some_other_condition
end
end
Pundit is after all just plain old Ruby OOP.
Pundit doesn't require you to be explicit, but it allows it. If the index? method in your policy wasn't duplicated, you'd want the ability to be explicit.
You can start by looking at moving some of the authorization checks into the set_material method, that cuts down over half of the checks.
The other half could be abstracted out into other private methods if you wanted, but I think they're fine as-is.
You could also look at adding a before_action callback to call the authorizer based on the action name, after you've memoized #material via your other callback, but readability is likely to suffer.
Use the second argument for the authorize method. Eg:
authorize #material, :index?
You can now remove all the other methods that just calls index?

pundit rails 5 can't enforce create method restrictions

everytime I submit a form here (that I scaffolded) localhost:3000/syllabus_requests/new
The rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
from my ApplicationController.rb file gets raised and I'm not sure why because in the policy class I have a create? method and it returns true
i'm using
ruby '2.3.1'
gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
gem 'pundit', '~> 1.1'
I have a policy
class SyllabusRequestPolicy < ApplicationPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user || User.new
#model = model #this is the syllabus_request record from the syllabus_requests table as a rails model object
end
def index?
#current_user.role == "admin"
end
def show?
#current_user.role == "admin"
end
def create?
true
end
def edit?
#current_user.role == "admin"
end
def update?
#current_user.role == "admin"
end
def destroy?
#current_user.role == "admin"
end
end
I have a controller
class SyllabusRequestsController < ApplicationController
before_action :set_syllabus_request, only: [:show, :edit, :update, :destroy]
# GET /syllabus_requests
# GET /syllabus_requests.json
def index
#syllabus_requests = SyllabusRequest.all
authorize #syllabus_requests
end
# GET /syllabus_requests/1
# GET /syllabus_requests/1.json
def show
authorize #syllabus_request
end
# GET /syllabus_requests/new
def new
#syllabus_request = SyllabusRequest.new
authorize #syllabus_request
end
# GET /syllabus_requests/1/edit
def edit
authorize #syllabus_request
end
# POST /syllabus_requests
# POST /syllabus_requests.json
def create
#syllabus_request = SyllabusRequest.new(syllabus_request_params)
authorize #syllabus_request
respond_to do |format|
if #syllabus_request.save
format.html { redirect_to #syllabus_request, notice: 'Syllabus request was successfully created.' }
format.json { render :show, status: :created, location: #syllabus_request }
else
format.html { render :new }
format.json { render json: #syllabus_request.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /syllabus_requests/1
# PATCH/PUT /syllabus_requests/1.json
def update
authorize #syllabus_request
respond_to do |format|
if #syllabus_request.update(syllabus_request_params)
format.html { redirect_to #syllabus_request, notice: 'Syllabus request was successfully updated.' }
format.json { render :show, status: :ok, location: #syllabus_request }
else
format.html { render :edit }
format.json { render json: #syllabus_request.errors, status: :unprocessable_entity }
end
end
end
# DELETE /syllabus_requests/1
# DELETE /syllabus_requests/1.json
def destroy
authorize #syllabus_request
#syllabus_request.destroy
respond_to do |format|
format.html { redirect_to syllabus_requests_url, notice: 'Syllabus request was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_syllabus_request
#syllabus_request = SyllabusRequest.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def syllabus_request_params
params.require(:syllabus_request).permit(:full_name, :email)
end
end
my ApplicationPolicy.rb file looks like this
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?
binding.pry # this should not hit if I'm overriding it
false
end
def new?
binding.pry
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
end
My ApplicationController.rb looks like this
include Pundit
protect_from_forgery with: :exception
before_action :configure_permitted_parameters, if: :devise_controller?
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
private
def user_not_authorized
# binding.pry
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
end
Did you try to add a new? method in your SyllabusRequestPolicy ?

Rails: No Pundit policy found in Rails

I have used the Pundit Gem before, but I've never tried doing what I'm trying to do now, and for some reason Pundit is not happy.
What I'm aiming to do, is to have a modal with the 'create' (Foo) form on my 'index'(Foos) page. Thus I need to instantiate an empty Foo object for the modal form to work.
The issue that I'm experiencing, is that Pundit throws an error when I submit the form remotely. The error is:
Pundit::NotDefinedError - unable to find policy of nil
I have tried to understand why this is happening but I've not been able to solve it yet.
Here is my foos_controller.rb#index:
...
def index
#foo = Foo.new
authorize #foo, :new?
#foos = policy_scope(Foo)
end
...
I then have the following 'before_action' filter that runs for my other actions i.e. 'create'
...
before_action :run_authorisation_check, except: [:index]
def run_authorisation_check
authorize #foo
end
...
The policies that I'm using in foo_policy.rb:
....
def index?
user.has_any_role? :super_admin
end
def create?
user.has_any_role? :super_admin
end
def new?
create?
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
if user.has_any_role? :super_admin
scope.all
end
end
end
....
The error does not present itself until I submit the form. Could anybody familiar with Pundit please help guide me to understand what I'm doing incorrectly?
UPDATE
Full foos_controller.rb
class FoosController < ApplicationController
def index
#foo = Foo.new
authorize #foo, :create?
#foos = policy_scope(Foo)
end
def new
#foo = Foo.new
end
def create
#foo = Foo.new(foo_params)
respond_to do |format|
if #foo.save
flash[:notice] = I18n.t("foo.flash.created")
format.json { render json: #foo, status: :ok }
else
format.json { render json: #foo.errors, status: :unprocessable_entity }
end
end
end
private
before_action :run_authorisation_check, except: [:index]
def foo_params
params.fetch(:foo, {}).permit(:bar)
end
def run_authorisation_check
authorize #foo
end
end
Yeah, you're not setting the value of #foo, that's why you're getting the error unable to find policy of nil.
Most times, you would have something like this in your foos_controller.rb:
before_action :set_foo, only: [:show, :edit, :update, :destroy]
before_action :run_authorisation_check, except: [:index]
...
private
def set_foo
#foo = Foo.find(params[:id])
end
Let me know if that works
I had this issue when working on a Rails 6 API only application with the Pundit gem.
I was running into the error below when I test my Pundit authorization for my controller actions:
Pundit::NotDefinedError - unable to find policy of nil
Here's how I solved:
Say I have a policy called SchoolPolicy:
class SchoolPolicy < ApplicationPolicy
attr_reader :user, :school
def initialize(user, school)
#user = user
#school = school
end
def index?
user.admin?
end
def show?
user.admin?
end
def new
create?
end
def edit
update?
end
def create
user.admin?
end
def update?
user.admin?
end
def destroy?
user.admin?
end
end
Then in my SchoolsController, I will have the following:
class Api::V1::SchoolsController < ApplicationController
before_action :set_school, only: [:show, :update, :destroy]
after_action :verify_authorized, except: :show
# GET /schools
def index
#schools = School.all
authorize #schools
render json: SchoolSerializer.new(#schools).serializable_hash.to_json
end
# GET /schools/1
def show
render json: SchoolSerializer.new(#school).serializable_hash.to_json
end
# POST /schools
def create
#school = School.new(school_params)
authorize #school
if #school.save
render json: SchoolSerializer.new(#school).serializable_hash.to_json, status: :created, location: api_v1_school_url(#school)
else
render json: #school.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /schools/1
def update
authorize #school
if #school.update(school_params)
render json: SchoolSerializer.new(#school).serializable_hash.to_json
else
render json: #school.errors, status: :unprocessable_entity
end
end
# DELETE /schools/1
def destroy
authorize #school
#school.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_school
#school = School.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def school_params
params.require(:school).permit(:name, :alias, :code)
end
end
Note:
I used an after_action callback to call the verify_authorized method to enforce authorization for the controller actions
I did not call the authorize method on the show action because it was skipped for authorization by me out of choice based on my design.
The instance variables called by the authorize method corresponds to the instance variable of the controller actions being called. So for the index action it is #schools and for the create action it is #school and so on.
That's all.
I hope this helps

Rails 4 with Pundit

I am trying to make an app in Rails 4.
I want to use Pundit for authorisations. I also use Devise for authentication and Rolify for role management.
I have a user model and am making my first policy, following along with this tutorial:
https://www.youtube.com/watch?v=qruGD_8ry7k
I have a users controller with:
class UsersController < ApplicationController
before_action :set_user, only: [:index, :show, :edit, :update, :destroy]
def index
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
# GET /users/:id.:format
def show
# authorize! :read, #user
end
# GET /users/:id/edit
def edit
# authorize! :update, #user
end
# PATCH/PUT /users/:id.:format
def update
# authorize! :update, #user
respond_to do |format|
if #user.update(user_params)
sign_in(#user == current_user ? #user : current_user, :bypass => true)
format.html { redirect_to #user, notice: 'Your profile was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
# GET/PATCH /users/:id/finish_signup
def finish_signup
# authorize! :update, #user
if request.patch? && params[:user] #&& params[:user][:email]
if #user.update(user_params)
#user.skip_reconfirmation!
sign_in(#user, :bypass => true)
redirect_to #user, notice: 'Your profile was successfully updated.'
else
#show_errors = true
end
end
end
# DELETE /users/:id.:format
def destroy
# authorize! :delete, #user
#user.destroy
respond_to do |format|
format.html { redirect_to root_url }
format.json { head :no_content }
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(policy(#user).permitted_attributes)
# accessible = [ :first_name, :last_name, :email ] # extend with your own params
# accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
# accessible << [:approved] if user.admin
# params.require(:user).permit(accessible)
end
end
And this is my first go at the User policy.
class UserPolicy < ApplicationPolicy
def initialize(current_user, user)
#current_user = current_user
#user = user
end
def index?
#current_user.admin?
end
def show?
#current_user.admin?
end
def edit?
#current_user.admin?
end
def update?
#current_user.admin?
end
def finish_signup?
#current_user = #user
end
def destroy?
return false if #current_user == #user
#current_user.admin?
end
private
def permitted_attributes
accessible = [ :first_name, :last_name, :email ] # extend with your own params
accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
accessible << [:approved] if user.admin
params.require(:user).permit(accessible)
end
end
My questions are:
The tutorial shows something called attr_reader. I have started learning rails from rails 4 so I don't know what these words mean. I think it has something to do with the old way of whitelisting user params in the controller, so I think I don't need to include this in my user policy. Is that correct?
is it right that i have to initialise the user model the way I have above (or is that only the case in models other than user, since I'm initialising current_user, it might already get the user initialised?
is it necessary to move the strong params to the policy, or will this work if I leave them in the controller?
The tutorial shows something called attr_reader. I have started learning rails from rails 4 so I don't know what these words mean. I think it has something to do with the old way of whitelisting user params in the controller, so I think I don't need to include this in my user policy. Is that correct?
No, it is very important.
attr_reader creates instance variables and corresponding methods that return the value of each instance variable. - From Ruby Official Documentation
Basically if you do
class A
attr_reader :b
end
a = A.new
you can do a.b to access b instance variable. It is important because in every policies you might allow read access of instance variables. #current_user and #user is instance variable.
is it right that i have to initialise the user model the way I have above (or is that only the case in models other than user, since I'm initialising current_user, it might already get the user initialised?
You have to initialise it manually. Currently, the way you did it is correctly. Good.
is it necessary to move the strong params to the policy, or will this work if I leave them in the controller?
It is the matter of choice. It will work even if you kept it into controller. Move to policy only if you want to whitelist attributes in quite complex way.
Note: device , pundit and rolify gem works good but there share some of the same functionality so be careful and consistence what to do with what.
For example, You can use devise_for :users , :students , :teachers which will give 3 different links to login the respective resources. You can do lot of things with it. You can further authenticate the urls as per the resources with authenticate method. Check https://github.com/plataformatec/devise/wiki/How-To:-Define-resource-actions-that-require-authentication-using-routes.rb This sort of thing can also be done with pundit with policies and rolify with roles.

Implementing scopes in Pundit

I am using the Pundit gem (with Devise and Rolify) to restrict access to information based on logged-in user roles.
At this time I have three roles for my User model defined: Admin, Client Admin, and Customer Admin.
A User belongs_to a Customer.
Customer has_many Users.
I have successfully implemented a Pundit policy when indexing the Customer model. Admins and Client Admins can see all Customers. Customer Admin can only see their OWN record.
The problem lies when I am trying to restrict the show method of the Customer controller. Admins and Client Admins can see all Customers. However, the Customer Admin should only be able to see his own record. But as it stands the Customer Admin can input any id in the URL and see any Customer record.
I'm fuzzy on the scoping. It's my understanding that the Policy methods (i.e. index? and show?) are to restrict WHO can perform these actions and the Scoping methods restrict WHICH RECORDS can be obtained. I'm having trouble composing the correct scope for the above scenario.
Here's the Customer controller:
class CustomersController < ApplicationController
before_action :set_customer, only: [:show, :edit, :update, :destroy]
after_action :verify_authorized
# GET /customers
# GET /customers.json
def index
#customers = policy_scope(Customer)
authorize Customer
end
# GET /customers/1
# GET /customers/1.json
def show
authorize #customer
end
# GET /customers/new
def new
#customer = Customer.new
authorize #customer
end
# GET /customers/1/edit
def edit
authorize #customer
end
# POST /customers
# POST /customers.json
def create
#customer = Customer.new(customer_params)
authorize #customer
respond_to do |format|
if #customer.save
format.html { redirect_to #customer, notice: 'Customer was successfully created.' }
format.json { render :show, status: :created, location: #customer }
else
format.html { render :new }
format.json { render json: #customer.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /customers/1
# PATCH/PUT /customers/1.json
def update
authorize #customer
respond_to do |format|
if #customer.update(customer_params)
format.html { redirect_to #customer, notice: 'Customer was successfully updated.' }
format.json { render :show, status: :ok, location: #customer }
else
format.html { render :edit }
format.json { render json: #customer.errors, status: :unprocessable_entity }
end
end
end
# DELETE /customers/1
# DELETE /customers/1.json
def destroy
authorize #customer
#customer.destroy
respond_to do |format|
format.html { redirect_to customers_url, notice: 'Customer was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_customer
#customer = Customer.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def customer_params
params.require(:customer).permit(:name, :parent_customer_id, :customer_type, :active, :currency)
end
end
And here is the Customer policy:
class CustomerPolicy < ApplicationPolicy
def index?
# Admins, ClientAdmins, and CustomerAdmins can index customers (see Scope class for filters)
#user.has_role? :admin or #user.has_role? :client_admin or #user.has_role? :customer_admin
end
def show?
# Admins, ClientAdmins, and CustomerAdmins can see any customer details
#user.has_role? :admin or #user.has_role? :client_admin or #user.has_role? :customer_admin
end
def update?
# Only Admins and ClientAdmins can update customer details
#user.has_role? :admin or #user.has_role? :client_admin
end
def destroy?
#user.has_role? :admin or #user.has_role? :client_admin
end
class Scope < Struct.new(:user, :scope)
def resolve
if (user.has_role? :admin or user.has_role? :client_admin)
# Admins and ClientAdmins can see all Customers
scope.where(:parent_id => nil)
elsif user.has_role? :customer_admin
# Customer Admins can only see their own Customer
scope.where(:id => user.customer) # THIS DOES NOT APPEAR TO GET INVOKED BY THE SHOW METHOD OF THE CONTROLLER
end
end
def show?
# NOT SURE WHAT TO PUT IN HERE
end
end
end
Success!! Thanks to the headstart given to me by railscard, the trick was to modify the show? method in the Customer policy file like the following:
def show?
# Admins, ClientAdmins, and CustomerAdmins can see any customer details
# Students cannot see customer details
return true if user.has_role?(:admin) || user.has_role?(:client_admin)
return true if user.customer_id == #record.id && user.has_role?(:customer_admin)
false
end
Note that I had to use the #record instance variable, as that's what the Application policy class uses to refer to the record being passed in by the authorize method.
Thanks!!
To get Pundit's scoping working for the show action, Pundit's policy_scope helper (or policy_scope!) could be used, or you could just inherit show? from the generated ApplicationPolicy.
The index action is already using policy_scope correctly, we just need to do something similar for the show action. Here are some options:
Option 1: Modify the show action to
def show
# Also remove :show from the :only option where
# before_action :set_customer, only: ... is called.
#customer = policy_scope(Customer).find(params[:id])
authorize #customer
end
OR
Option 2: Modify set_customer to
def set_customer
#customer = policy_scope(Customer).find(params[:id])
end
OR
Option 3: Modify CustomerPolicy#show? to
def show?
# scope call here will return the
# result of CustomerPolicy::Scope#resolve
# This is the same implementation generated
# in the default ApplicationPolicy so you could
# just delete this method here and inherit instead.
scope.where(:id => record.id).exists?
end
Here's the code that generates the default ApplicationPolicy#show? method.
See Pundit's README section on Scopes for additional details.
I think you can safely delete the empty show? method you have in CustomerPolicy::Scope, I don't believe it will be called.
I think you don't need scope to restrict access for show action.
def show?
return true if user.has_role? :admin || user.has_role? :client_admin
return true if user.customer_id == customer.id && user.has_role? :customer_admin
false
end
Pundit scopes usually used to fetch a list of records which user have access to. In case of show method (or any other method in controller, where you call authorize) Pundit instantiates policy class with current user and given customer and then simply calls show? method to check user permissions, i.e. CustomerPolicy.new(current_user, #customer).show?

Resources