lib/cancan/rule.rb
# frozen_string_literal: true require_relative 'conditions_matcher.rb' require_relative 'class_matcher.rb' require_relative 'relevant.rb' module CanCan # This class is used internally and should only be called through Ability. # it holds the information about a "can" call made on Ability and provides # helpful methods to determine permission checking and conditions hash generation. class Rule # :nodoc: include ConditionsMatcher include Relevant include ParameterValidators attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes, :block attr_writer :expanded_actions, :conditions # The first argument when initializing is the base_behavior which is a true/false # value. True for "can" and false for "cannot". The next two arguments are the action # and subject respectively (such as :read, @project). The third argument is a hash # of conditions and the last one is the block passed to the "can" call. def initialize(base_behavior, action, subject, *extra_args, &block) # for backwards compatibility, attributes are an optional parameter. Check if # attributes were passed or are actually conditions attributes, extra_args = parse_attributes_from_extra_args(extra_args) condition_and_block_check(extra_args, block, action, subject) @match_all = action.nil? && subject.nil? raise Error, "Subject is required for #{action}" if action && subject.nil? @base_behavior = base_behavior @actions = wrap(action) @subjects = wrap(subject) @attributes = wrap(attributes) @conditions = extra_args || {} @block = block end def inspect repr = "#<#{self.class.name}" repr += "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}" if with_scope? repr += ", #{@conditions.where_values_hash}" elsif [Hash, String].include?(@conditions.class) repr += ", #{@conditions.inspect}" end repr + '>' end def can_rule? base_behavior end def cannot_catch_all? !can_rule? && catch_all? end def catch_all? (with_scope? && @conditions.where_values_hash.empty?) || (!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions)) end def only_block? conditions_empty? && @block end def only_raw_sql? @block.nil? && !conditions_empty? && !@conditions.is_a?(Hash) end def with_scope? defined?(ActiveRecord) && @conditions.is_a?(ActiveRecord::Relation) end def associations_hash(conditions = @conditions) hash = {} if conditions.is_a? Hash conditions.map do |name, value| hash[name] = associations_hash(value) if value.is_a? Hash end end hash end def attributes_from_conditions attributes = {} if @conditions.is_a? Hash @conditions.each do |key, value| attributes[key] = value unless [Array, Range, Hash].include? value.class end end attributes end def matches_attributes?(attribute) return true if @attributes.empty? return @base_behavior if attribute.nil? @attributes.include?(attribute.to_sym) end private def matches_action?(action) @expanded_actions.include?(:manage) || @expanded_actions.include?(action) end def matches_subject?(subject) @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject) end def matches_subject_class?(subject) SubjectClassMatcher.matches_subject_class?(@subjects, subject) end def parse_attributes_from_extra_args(args) attributes = args.shift if valid_attribute_param?(args.first) extra_args = args.shift [attributes, extra_args] end def condition_and_block_check(conditions, block, action, subject) return unless conditions.is_a?(Hash) && block raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. ' \ "Check \":#{action} #{subject}\" ability." end def wrap(object) if object.nil? [] elsif object.respond_to?(:to_ary) object.to_ary || [object] else [object] end end end end