lib/kojac/kojac_rails.rb in kojac-0.9.1 vs lib/kojac/kojac_rails.rb in kojac-0.11.0

- old
+ new

@@ -1,6 +1,6 @@ -#require File.expand_path('ring_strong_parameters',File.dirname(__FILE__)) +require 'pundit' Kernel.class_eval do def key_join(aResource,aId=nil,aAssoc=nil) result = aResource if aId @@ -59,48 +59,143 @@ aValue = aValue.map {|v| upgrade_hashes_to_params(v)} end aValue end + def timestamp + Time.now.to_ms + end + + def serializer_for(aObject) + return ::KojacBaseSerializer if aObject.is_a?(Hash) + return ::ActiveModel::ArraySerializer if aObject.respond_to?(:to_ary) + return ActiveModel::DefaultSerializer if KojacBaseSerializer::SERIALIZABLE_TYPES.include?(aObject.class) + return aObject.send(:active_model_serializer) if aObject.respond_to?(:active_model_serializer) + return aObject.class.send(:active_model_serializer) if aObject.class.respond_to?(:active_model_serializer) + if sz_class = ActiveModel::Serializer.serializer_for(aObject) + sz_class + else + aObject.respond_to?(:attributes) ? ::KojacBaseSerializer : ActiveModel::DefaultSerializer + end + end + + def to_jsono(aObject,aOptions) + if aObject.is_a?(Hash) or aObject.respond_to? :attributes + serializer_for(aObject).new(aObject,aOptions).serializable_object + elsif aObject.respond_to?(:to_ary) && aObject.first # Array + item_sz = serializer_for(aObject.first) + ActiveModel::ArraySerializer.new(aObject,aOptions.merge(each_serializer: item_sz)).as_json(aOptions) + else + aObject.as_json(aOptions) + end + end + + def to_json(aObject,aOptions) + jsono = to_jsono(aObject,aOptions) + result = jsono.to_json + result + end end module Kojac module ModelMethods def self.included(aClass) aClass.send :extend, ClassMethods + aClass.class_eval do + scope :by_key, ->(aKey,aOperation=nil) { + key = if respond_to?(:crack_key) + crack_key(aKey) + elsif aClass.respond_to?(:crack_key) + aClass.crack_key(aKey) + end + r = key[:resource] + id = key[:id] + a = key[:association] + if id + where(id: id) + else + where('1 = 1') + end + } + end end module ClassMethods - def by_key(aKey,aContext=nil) + + # used by pundit + def policy_class + "#{self}Policy".safe_constantize || KojacBasePolicy + end + + def create_policy(aCurrentUser,aOp=nil) + policy_class.new(aCurrentUser,self,aOp) + end + + def active_model_serializer + "#{self}Serializer".safe_constantize || KojacBaseSerializer + end + + def crack_key(aKey) r,id,a = aKey.split_kojac_key - model = self - model = self.rescope(model,aContext) if self.respond_to? :rescope + result = {} + result[:original] = aKey + result[:resource] = r if r + result[:id] = id if id + result[:association] = a if a + result + end + + def load_by_key(aKey,aOperation=nil) + r,id,a = aKey.split_kojac_key + rel = by_key(aKey) if id - model.where(id: id).first + result = rel.first + result.prepare(aKey,aOperation) if result.respond_to? :prepare else - model.all + result = rel.all + result.each do |item| + item.prepare(aKey,aOperation) if item.respond_to? :prepare + end end + result end end + def unauthorized!(aMessage=nil) + raise ::Pundit::NotAuthorizedError, aMessage||"You are not authorized to perform this action" + end + def kojac_key self.class.to_s.snake_case.pluralize+'__'+self.id.to_s end - def update_permitted_attributes!(aChanges, aRing) + def update_permitted_attributes!(aChanges, aPolicy) aChanges = KojacUtils.upgrade_hashes_to_params(aChanges) - permitted_fields = self.class.permitted_fields(:write, aRing) - permitted_fields = aChanges.permit(*permitted_fields) - assign_attributes(permitted_fields, :without_protection => true) + p_fields = aPolicy.permitted_fields(:write) + unauthorized! if p_fields.empty? + p_fields = aChanges.permit(*p_fields) + if ::Rails::VERSION::MAJOR <= 3 + assign_attributes(p_fields, :without_protection => true) + else + assign_attributes(p_fields) + end save! end + def as_json(options = nil) + super + end + end end module Kojac + + class NotFoundError < StandardError + end + module ControllerOpMethods def self.included(aClass) #aClass.send :extend, ClassMethods aClass.send :include, ActiveSupport::Callbacks @@ -126,31 +221,42 @@ def kojac_resource self.class.to_s.chomp('Controller').snake_case end + def kojac_current_user + self.current_user + end + + def current_ring + kojac_current_user.try(:ring).to_i + end + def create_on_association(aItem,aAssoc,aValues,aRing) - raise "User does not have permission for create on #{aAssoc}" unless aItem.class.permitted_associations(:create,aRing).include?(aAssoc.to_sym) + raise "User does not have permission for create on #{aAssoc}" unless aItem.class.ring_can?(aRing,:create_on,aAssoc.to_sym) return nil unless ma = aItem.class.reflect_on_association(aAssoc.to_sym) a_model_class = ma.klass + policy = Kojac.policy!(kojac_current_user,a_model_class) aValues = KojacUtils.upgrade_hashes_to_params(aValues || {}) case ma.macro when :belongs_to return nil if !aValues.is_a?(Hash) - fields = aValues.permit( *a_model_class.permitted_fields(:write,aRing) ) + fields = aValues.permit( *policy.permitted_fields(:write) ) + a_model_class.write_op_filter(current_user,fields,aValues) if a_model_class.respond_to? :write_op_filter return aItem.send("build_#{aAssoc}".to_sym,fields) when :has_many aValues = [aValues] if aValues.is_a?(Hash) return nil unless aValues.is_a? Array aValues.each do |v| - fields = v.permit( *a_model_class.permitted_fields(:write,aRing) ) + fields = v.permit( *policy.permitted_fields(:write) ) new_sub_item = nil case ma.macro when :has_many + a_model_class.write_op_filter(current_user,fields,aValues) if a_model_class.respond_to? :write_op_filter new_sub_item = aItem.send(aAssoc.to_sym).create(fields) else raise "#{ma.macro} association unsupported in CREATE" end merge_model_into_results(new_sub_item) @@ -171,44 +277,48 @@ # next #end end def create_op - ring = current_user.try(:ring) + ring = current_ring op = params[:op] options = op[:options] || {} model_class = deduce_model_class resource,id,assoc = op['key'].split_kojac_key if assoc # create operation on an association eg. {verb: "CREATE", key: "order.items"} - raise "User does not have permission for #{op[:verb]} operation on #{model_class.to_s}.#{assoc}" unless model_class.permitted_associations(:create,ring).include?(assoc.to_sym) + raise "User does not have permission for #{op[:verb]} operation on #{model_class.to_s}.#{assoc}" unless model_class.ring_can?(ring,:create_on,assoc.to_sym) item = KojacUtils.model_for_key(key_join(resource,id)) ma = model_class.reflect_on_association(assoc.to_sym) a_value = op[:value] # get data for this association, assume {} raise "create multiple not yet implemented for associations" unless a_value.is_a?(Hash) a_model_class = ma.klass - p_fields = a_model_class.permitted_fields(:write,ring) + policy = Kojac.policy!(kojac_current_user,a_model_class) + p_fields = policy.permitted_fields(:write) fields = a_value.permit( *p_fields ) new_sub_item = nil case ma.macro when :has_many + a_model_class.write_op_filter(current_user,fields,a_value) if a_model_class.respond_to? :write_op_filter new_sub_item = item.send(assoc.to_sym).create(fields) else raise "#{ma.macro} association unsupported in CREATE" end result_key = op[:result_key] || new_sub_item.kojac_key merge_model_into_results(new_sub_item) else # create operation on a resource eg. {verb: "CREATE", key: "order_items"} but may have embedded association values - p_fields = model_class.permitted_fields(:write,ring) + policy = Kojac.policy!(kojac_current_user,model_class) + p_fields = policy.permitted_fields(:write) raise "User does not have permission for #{op[:verb]} operation on #{model_class.to_s}" unless model_class.ring_can?(:create,ring) p_fields = op[:value].permit( *p_fields ) + model_class.write_op_filter(current_user,p_fields,op[:value]) if model_class.respond_to? :write_op_filter item = model_class.create!(p_fields) options_include = options['include'] || [] included_assocs = [] - p_assocs = model_class.permitted_associations(:write,ring) + p_assocs = policy.permitted_associations(:write) if p_assocs p_assocs.each do |a| next unless (a_value = op[:value][a]) || options_include.include?(a.to_s) create_on_association(item,a,a_value,ring) included_assocs << a.to_sym @@ -227,19 +337,20 @@ end protected def merge_model_into_results(aItem,aResultKey=nil,aOptions=nil) - ring = current_user.try(:ring) - aResultKey ||= aItem.kojac_key + ring = current_ring + aResultKey ||= aItem.g? :kojac_key + results[aResultKey] = (aItem && KojacUtils.to_jsono(aItem,scope: kojac_current_user)) + return unless policy = Kojac.policy!(kojac_current_user,aItem) aOptions ||= {} - results[aResultKey] = aItem.sanitized_hash(ring) if included_assocs = aOptions[:include] included_assocs = included_assocs.split(',') if included_assocs.is_a?(String) included_assocs = [included_assocs] unless included_assocs.is_a?(Array) included_assocs.map!(&:to_sym) if included_assocs.is_a?(Array) - p_assocs = aItem.class.permitted_associations(:read,ring) + p_assocs = policy.permitted_associations(:read) # *** use_assocs = p_assocs.delete_if do |a| if included_assocs.include?(a) and ma = aItem.class.reflect_on_association(a) ![:belongs_to,:has_many].include?(ma.macro) # is supported association type else true # no such assoc @@ -248,16 +359,16 @@ use_assocs.each do |a| next unless a_contents = aItem.send(a) if a_contents.is_a? Array contents_h = [] a_contents.each do |sub_item| - results[sub_item.kojac_key] = sub_item.sanitized_hash(ring) + results[sub_item.kojac_key] = KojacUtils.to_jsono(sub_item,scope: kojac_current_user) #contents_h << sub_item.id end #results[aResultKey] = contents_h else - results[a_contents.kojac_key] = a_contents.sanitized_hash(ring) + results[a_contents.kojac_key] = KojacUtils.to_jsono(a_contents,scope: kojac_current_user) end end end end @@ -267,30 +378,43 @@ op = params[:op] key = op[:key] result_key = nil resource,id = key.split '__' model = deduce_model_class - + scope = Kojac.policy_scope(current_user, model, op) || model if id # item - if model - item = model.by_key(key,op) + if scope + item = scope.load_by_key(key,op) + #item = item.first + #item.prepare(key,op) if item.respond_to? :prepare result_key = op[:result_key] || (item && item.kojac_key) || op[:key] merge_model_into_results(item,result_key,op[:options]) else result_key = op[:result_key] || op[:key] results[result_key] = null end else # collection result_key = op[:result_key] || op[:key] results[result_key] = [] - if model - items = model.by_key(key,op) - items.each do |m| - item_key = m.kojac_key - results[result_key] << item_key.bite(resource+'__') - merge_model_into_results(m,item_key,op[:options]) + if scope + items = scope.load_by_key(key,op) + #items = scope.by_key(key,op) + #items = items.all + items.each do |item| + item.prepare(key,op) if item.respond_to? :prepare end + if op[:options] and op[:options][:atomise]==false + items_json = [] + items_json = items.map {|i| KojacUtils.to_jsono(i,scope: kojac_current_user) } + results[result_key] = items_json + else + items.each do |m| + item_key = m.kojac_key + results[result_key] << item_key.split_kojac_key[1] + merge_model_into_results(m,item_key,op[:options]) + end + end end end { key: op[:key], verb: op[:verb], @@ -299,43 +423,44 @@ } end def update_op result = nil - model = deduce_model_class - - ring = current_user.try(:ring) + ring = current_ring op = params[:op] result_key = nil - if self.item = model.by_key(op[:key],op) + model = deduce_model_class + scope = Kojac.policy_scope(current_user, model, op) || model + if self.item = scope.load_by_key(op[:key],op) + run_callbacks :update_op do - item.update_permitted_attributes!(op[:value], ring) + policy = Kojac.policy!(kojac_current_user,item,op) + item.update_permitted_attributes!(op[:value], policy) - associations = model.permitted_associations(:write,ring) + associations = policy.permitted_associations(:write) associations.each do |k| next unless assoc = model.reflect_on_association(k) next unless op[:value][k] case assoc.macro when :belongs_to if leaf = (item.send(k) || item.send("build_#{k}".to_sym)) - #permitted_fields = leaf.class.permitted_fields(:write,ring) - #permitted_fields = op[:value][k].permit( *permitted_fields ) - #leaf.assign_attributes(permitted_fields, :without_protection => true) - #leaf.save! - leaf.update_permitted_attributes!(op[:value][k], ring) + policy = Kojac.policy!(kojac_current_user,leaf) + leaf.update_permitted_attributes!(op[:value][k], policy) end end end result_key = item.kojac_key - results[result_key] = item + #results[result_key] = item + merge_model_into_results(item,result_key,op[:options]) associations.each do |a| next unless assoc_item = item.send(a) next unless key = assoc_item.respond_to?(:kojac_key) && assoc_item.kojac_key - results[key] = assoc_item + #results[key] = assoc_item + merge_model_into_results(assoc_item,key) end end end { key: op[:key], @@ -344,11 +469,11 @@ results: results } end def destroy_op - ring = current_user.try(:ring) + ring = current_ring op = params[:op] result_key = op[:result_key] || op[:key] item = KojacUtils.model_for_key(op[:key]) item.destroy if item results[result_key] = nil @@ -363,11 +488,11 @@ #def execute_op # puts 'execute_op' #end def add_op - ring = current_user.try(:ring) + ring = current_ring op = params[:op] model = deduce_model_class raise "ADD only supports associated collections at present eg order.items" unless op[:key].index('.') item = KojacUtils.model_for_key(op[:key].base_key) @@ -396,11 +521,11 @@ results: results } end def remove_op - ring = current_user.try(:ring) + ring = current_ring op = params[:op] model = deduce_model_class raise "REMOVE only supports associated collections at present eg order.items" unless op[:key].key_assoc item = KojacUtils.model_for_key(op[:key].base_key) @@ -427,8 +552,34 @@ key: op[:key], verb: op[:verb], result_key: result_key, results: results } + end + + def execute_op + op = params[:op] + resource,action = op[:key].split_kojac_key + raise "action not given" unless action.is_a? String + action = "execute_#{action}" + raise "action #{action} not implemented on #{resource}" unless respond_to? action.to_sym + result = send(action.to_sym,op) + if op[:error] + { + key: op[:key], + verb: op[:verb], + error: op[:error] + } + else + result_key = op[:result_key] || op[:key] + results = op[:results] || {} # look at op[:results][result_key]. If empty, fill with returned value from action + results[result_key] = KojacUtils.to_jsono(result,scope: kojac_current_user) unless results.has_key? result_key + { + key: op[:key], + verb: op[:verb], + result_key: result_key, + results: results + } + end end end end \ No newline at end of file