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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With