Implementing scopes in Pundit
pundit, ruby-on-rails, ruby-on-rails-4
Solution
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?`
Problem
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!!