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) <:)
I'd like a (super)user to be able to assign authorizations and permissions like this
The ACL system should be lazy – ie if no roles/authorizations are given, the users obviously does not concern themselves with ACL at all.
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
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
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
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 })
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
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)
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