Search code examples
joinsubquerysqueel

how to join a subquery with conditions in Squeel


Prologue

I've embraced Squeel – and enjoying every step! Thank you so much for sharing, Ernie Miller!

I'm developing with ruby 1.9.2 and Squeel 1.0.2 and Rails 3.2.5

(I'll confess to having restructured the question entirely - hoping to increase the readability and better my chances of getting an answer) <:)

Use case

I'd like a (super)user to be able to assign authorizations and permissions like this

  • a user_group should be able to have multiple authorizations
  • an authorization should be able to have multiple permissions
  • a permission should be able to control access to (manipulating) data
    • via the controller (the request path)
    • on instances of a Class
    • on any particular instance

The ACL system should be lazy – ie if no roles/authorizations are given, the users obviously does not concern themselves with ACL at all.

Migrations

I identified role and (a polymorphic) roleable entities from the use case an thus I have

a Role right out of the ordinary

create_table :roles do |t|
  t.references :ox
  t.string :name
  t.boolean :active, default: true
  t.timestamps
end

and a Roleable a bit more descriptive

create_table :roleables do |t|
  t.references :ox
  t.references :role
  t.references :roleable, polymorphic: true
  t.string :authorization
  t.string :controller
  t.boolean :active, default: true
  t.timestamps
end

Classes

The system has a generic class - AbstractActionBase - which inherits from ActiveRecord:Base, and which all classes inherits from (allowing me to add systemwide attributes and methods in one place)

So - in part - my AbstractActionBase looks like

class AbstractActionBase < ActiveRecord::Base
  self.abstract_class=true

  require 'tempfile'

  belongs_to :ox

  has_many :roleables, as: :roleable

  attr_accessible :ox_id

  validates_presence_of :ox_id

  #
  # all models inheriting from this will have versions
  has_paper_trail
  #
  #

  # 
  # Class method to providing for index SELECT's being married with roleables (permissions)
  # used from abstraction_actions_controller where build_collection calls this method 
  # the result 'should' be an ActiveRelation - used for the Kamanari 'result' call to readying pagination
  #
  def self.with_authorizations    

    # 
    # SELECT * FROM any_table at
    # left join (
    #     select r.roleable_id, r.roleable_type, group_concat( r.authorization )
    #   from roleables r
    #   where r.authorization is not null
    #   and r.roleable_id=at.id
    #   and r.roleable_type=at.base_class
    #   and r.role_id not in (1,2,3) <--- ID's are current_user.roles
    # ) rm on rm.roleable_id=at.id and rm.roleable_type=at.base_class
    #
    # which will provide for this:
    #
    # |.......| last column in table 'at' | roleable_id | roleable_type | authorizations |
    # |.......| some value                | 1           | 'UserGroup'   | 'insert,create'|
    # |.......| yet another value         | 92          | 'UserGroup'   | 'read'         |
    #
    #
    self.where{ active==true }
  end

  # compile a collection of records - regard search using Ransack
  def base.collection( params, resource_set )
    #
    # kaminari (and continous scrolling)
    #
    params[:page] ||= 1
    params[:per_page] ||= self.per_page
    params[:o] ||= self.resource_order_by
    distinct = params[:distinct].nil? ? false : params[:distinct].to_i.zero?
    resource_set = (resource_set.respond_to?( "result")) ? resource_set.result(:distinct => distinct) : resource_set
    (resource_set.respond_to?( "page")) ? resource_set.order(params[:o]).page( params[:page] ).per( params[:per_page] ) : resource_set.order(params[:o])
  end
end

Part of the Role class looks like this

class Role < AbstractActionBase

  has_many :roleables

  scope :active, where{ active.eq true }

  #
  # what does this role allow
  def permissions
    roleables.permissions.scoped
  end

  # 
  # to whom does this role allow
  def authorizations
    roleables.authorizations.scoped
  end

  # returns true if the roleables (permissions) authorizes the options
  # options are { controller: "", action: "", record: Instance, is_class: boolean }
  def authorizes?( options={} )
    coll = permissions
    coll = coll.on_action(options.delete(:action)) if options.keys.include? :action
    coll = coll.on_entity( options.delete(:record), options.delete(:is_class) || false ) if options.keys.include? :record
    coll = coll.on_controller(options.delete(:controller)) if options.keys.include? :controller
    (coll.count>0) === true
  end
end

The Roleable class looks like this

class Roleable  < AbstractActionBase
  belongs_to :role
  belongs_to :roleable, polymorphic: true

  # roleables authorizes users through user_groups
  # (in which case the authorization is "-")
  # providing them permissions on controllers, actions and instances
  scope :authorizations, where{ authorization == nil }
  scope :permissions, where{ authorization != nil }

  # using Squeel, find roleables on a particular controller or any controller
  def self.on_controller(ctrl)
    where{ (controller==ctrl) | (controller==nil) }
  end

  # using Squeel, find roleables on a particular authorization or allowed 'all'
  def self.on_action(action)
    where{ (authorization=~ "%#{action}%") | (authorization=="all") }
  end

  # using Squeel, find roleables on a particular instance/record or class
  def self.on_entity(entity, is_class=false)
    if is_class
      where{ ((roleable_type==entity.base_class.to_s ) & ( roleable_id==nil)) | ((roleable_type==nil) & (roleable_id==nil)) }
    else
      where{ ((roleable_type==entity.class.to_s ) & ( roleable_id==entity.id)) | ((roleable_type==nil) & (roleable_id==nil)) }
    end
  end
end

Logic

Creating

This allows me authorizations - assigning roles to someone/something - in which case the authorization string is nil, like

The user_group sales is assigned the role sales with Roleable.create({ role: @sales, roleable: @user_group })

At the same time I can do permissions - describing the particulars of any role - like

The role sales has index, create, edit and delete permissions on the OrderHead and OrderDetail tables with

  • Roleable.create({ role: @sales, authorization: "index,create,edit,delete", roleable: @user_group, controller: "order_heads" })
  • Roleable.create({ role: @sales, authorization: "index,create,edit,delete", roleable: @user_group, controller: "order_details" })

these 'particulars' can be ethereal like

Roleable.create({ role: @sales, authorization: "index" })

somewhat real

Roleable.create({ role: @sales, authorization: "index", roleable_type: 'OrderHead' })

or very expressed

Roleable.create({ role: @sales, authorization: "index", roleable: OrderHead.first })

Selecting

Most every controller inherits from AbstractActionsController where the index (and other actions) are defined. That controller it self inherits from InheritedResources:Base like this

class AbstractActionsController < InheritedResources::Base # < ApplicationController

  append_view_path ViewTemplate::Resolver.instance

  respond_to :html, :xml, :json, :js, :pdf

  belongs_to :ox, :optional => true

  before_filter :authorize!
  before_filter :authenticate!
  before_filter :warn_unless_confirmed!
  before_filter :fix_money_params, :only => [:create,:update]

  # GET /collection - printers
  def index

    # session[:params] = params
    #
    # preparing for Ransack
    unless params[:q].nil?
      params[:q]= { :"#{params[:q_fields]}" => params[:q] }
    end

    super do |format|
      format.html 
      format.js { render layout: false }
      format.pdf{ render :pdf => generate_pdf(false) and return }
      format.xml { render layout: false }
      format.json do
        # field lookup request?
        unless params[:lookup].nil?
          render layout: false, :json => collection.map(&:select_mapping)
        else
          render json: collection.map { |p| view_context.grow_mustache_for_index(p, collection, (parent? ? collection : resource_class.order(:id)), @selected ) }
        end
      end
    end
  end


  # the collection method on inherited_resources 
  # gets overloaded with Ransack search and Kaminari pagination (on the model)
  def collection
    # @collection ||= build_collection
    # TODO - test whether caching the collection is possible
    build_collection
  end

  def build_collection
    unless params[:belongs].nil?
      # debugger
      parent = params[:belongs].constantize.find(params[:belongs_id])
      @selected = parent.nil? ? [] : parent.send( rewrite_association(params[:assoc],parent) )
      @search_resource = core_entity(params[:assoc].constantize)
      @search_resource = @search_resource.search(params[:q]) unless params[:q].nil?
    else
      @search_resource = rewrite_end_of_association_chain(resource_class)
      @search_resource = core_entity(@search_resource)
      @search_resource = @search_resource.search(params[:q]) unless params[:q].nil?
    end
    # authorize rows
    @search_resource = @search_resource.with_authorizations                 # left joins roleables coalescing a "authorization" field from roles ID's not owned by current_user through his user_groups
    @resources ||= resource_class.collection( params, @search_resource )
  end

end

Challenge

What a long story to presenting a short question <:)

How do I write the with_authorizations method to returning a ActiveRelation (and preferably using Squeel)


Solution

  • Like I said - preferably using Squeel :)

    It turns out that (from the horses mouth so to speak) joins are for associations in Squeel-county ;)

    So - what to do? Well, I did one last tour de SO with my SQL-to-ActiveRecord lasso swinging, and lo' and behold! Someone had asked a great question - and there was an even greater answer! Perfect.

    In a few short almost fever blinded moments I hacked away using the technique described - and Heureka !!

    Previously, I added a pastiebin to aid possible "answerees" - so I've added the result to the pastiebin - but in short it goes like this:

    Model.select("something").joins("to your hearts contend")
    

    Cheers, Walther