0

I am struggling to authorize an index for a model that doesn't have a direct relationship with my User model. Actually, I am struggling to wrap my head around the idea of Pundit scopes.

I understand that I can't authorize @sites within my SitePolicy, and from all the reading I have done, I believe I just need to do it in a Scope within the SitePolicy Class, but am unsure how to do this.

Here's what I have:

Models

User
  has_one :business
  has_many :locations, :through => :business
end

Business
  belongs_to :user
  has_many :locations
end

Location
  extend FriendlyId
  belongs_to :business
  has_one :user, :through => :business
  has_many :sites, dependent: :destroy
  friendly_id :custom_url, use: :slugged
end

Site
  belongs_to :location
end

routes.rb

 resources :locations do
    resources :sites
  end

sites_controller.rb

class SitesController < ApplicationController
  before_action :set_site, only: [:show, :edit, :update, :destroy]
  before_action :set_location, only: [:new, :show, :edit, :index, :update, :destroy]


  def index
    @sites = @location.sites.all
    authorize Site
  end

  private
    def set_site
      @site = Site.find(params[:id])
    end
    def set_location
      @location = Location.friendly.find(params[:location_id])
    end
    def site_params
      params.require(:site).permit(:location_id, :site, :url, :review_site_id, :number_of_reviews, :average_rating, :extra_data)
    end
end

site_policy.rb

class SitePolicy < ApplicationPolicy
    class Scope
      attr_reader :user, :scope

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

      def resolve
        if user.has_role? :admin
          scope.all
        else
          scope.where(scope.location.user == user)
        end
      end
    end

  def index? 
    return true if user.present? and user.has_role? :admin
  end
  ...

schema.rb

ActiveRecord::Schema.define(version: 2018_05_28_085645) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "businesses", force: :cascade do |t|
    t.string "name"
    t.text "description"
    t.bigint "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.boolean "more_than_one_location"
    t.boolean "signed_up"
    t.string "slug"
    t.index ["more_than_one_location"], name: "index_businesses_on_more_than_one_location"
    t.index ["user_id"], name: "index_businesses_on_user_id"
  end

  create_table "friendly_id_slugs", force: :cascade do |t|
    t.string "slug", null: false
    t.integer "sluggable_id", null: false
    t.string "sluggable_type", limit: 50
    t.string "scope"
    t.datetime "created_at"
    t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true
    t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type"
    t.index ["sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_id"
    t.index ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type"
  end

  create_table "locations", force: :cascade do |t|
    t.string "location_name"
    t.string "address_line_1"
    t.string "address_line_2"
    t.string "city"
    t.string "region"
    t.string "country"
    t.string "postal_code"
    t.string "website"
    t.string "phone_number"
    t.string "location_contact_email"
    t.bigint "business_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "slug", null: false
    t.string "custom_url", null: false
    t.index ["business_id"], name: "index_locations_on_business_id"
    t.index ["custom_url"], name: "index_locations_on_custom_url", unique: true
    t.index ["slug"], name: "index_locations_on_slug", unique: true
  end

  create_table "roles", force: :cascade do |t|
    t.string "name"
    t.string "resource_type"
    t.bigint "resource_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
    t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id"
  end

  create_table "sites", force: :cascade do |t|
    t.bigint "location_id"
    t.string "site"
    t.string "url"
    t.string "review_site_id"
    t.integer "number_of_reviews"
    t.decimal "average_rating"
    t.jsonb "extra_data", default: {}, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["extra_data"], name: "index_sites_on_extra_data", using: :gin
    t.index ["location_id"], name: "index_sites_on_location_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer "sign_in_count", default: 0, null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.inet "current_sign_in_ip"
    t.inet "last_sign_in_ip"
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

  create_table "users_roles", id: false, force: :cascade do |t|
    t.bigint "user_id"
    t.bigint "role_id"
    t.index ["role_id"], name: "index_users_roles_on_role_id"
    t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"
    t.index ["user_id"], name: "index_users_roles_on_user_id"
  end

  add_foreign_key "businesses", "users"
  add_foreign_key "locations", "businesses"
  add_foreign_key "sites", "locations"
end
Rich
  • 590
  • 7
  • 20

1 Answers1

3

To authorize collections of records, you normally do this with a scope rather than authorizing the action itself. In other words, instead of this:

def index
  @sites = @location.sites.all
  authorize Site
end

You need to do this:

def index
  @sites = policy_scope(@location.sites)
end

Since the link between a site and the user goes through several models, you unfortunately need to JOIN all the way back to the user in order to perform this query in SQL:

class SitePolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.has_role? :admin
        scope.all
      else
        scope.joins(location: :business)
             .where(locations: {businesses: {user: user}})
      end
    end
  end
end

Following this design pattern is why Pundit recommends adding the following code to your application:

class ApplicationController < ActionController::Base
  include Pundit
  after_action :verify_authorized, except: :index
  after_action :verify_policy_scoped, only: :index
end
Tom Lord
  • 27,404
  • 4
  • 50
  • 77
  • Hey Tom, thanks for helping me on this! I'm getting the following error when I run that: ``` PG::UndefinedTable: ERROR: missing FROM-clause entry for table "business" LINE 1: ...business_id" WHERE "sites"."location_id" = $1 AND "business"... ^ : SELECT "sites".* FROM "sites" INNER JOIN "locations" ON "locations"."id" = "sites"."location_id" INNER JOIN "businesses" ON "businesses"."id" = "locations"."business_id" WHERE "sites"."location_id" = $1 AND "business"."users" = $2 ``` I have updated my initial question with my Schema.db if that helps? – Rich Jun 01 '18 at 14:25
  • 1
    I did say I probably got the pluralisation slightly wrong... You probably need to `joins(locations: :businesses)` or something. – Tom Lord Jun 01 '18 at 14:46
  • You certainly did warn about that :) I'll play around with it and see what I can do - SQL is another weak point for me. Thank you for your help so far! – Rich Jun 01 '18 at 14:49
  • Ah, so changing that to your suggested `joins(locations: :businesses)` got me the following error: `Can't join 'Site' to association named 'locations'; perhaps you misspelled it?` I will keep playing with this. Thanks so much! – Rich Jun 01 '18 at 14:51
  • So.... Maybe it was the part within the `where` that actually needs pluralising? :P – Tom Lord Jun 01 '18 at 15:07