lib/acl9/controller_extensions/dsl_base.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require_relative "../prepositions"

module Acl9
  module Dsl
    class Base
      include Prepositions

      attr_reader :allows, :denys

      def initialize(*args)
        @default_action = nil
        @allows = []
        @denys  = []
        @original_args = args
        @action_clause = nil
      end

      def acl_block!(&acl_block)
        instance_eval(&acl_block)
      end

      def default_action
        @default_action.nil? ? :deny : @default_action
      end

      def allowance_expression
        allowed_expr    = @allows.any?  ? @allows.map { |clause| "(#{clause})" }.join(' || ') : 'false'
        not_denied_expr = @denys.any?   ? @denys.map { |clause| "!(#{clause})" }.join(' && ') : 'true'

        [allowed_expr, not_denied_expr].
          map { |expr| "(#{expr})" }.
          join(default_action == :deny ? ' && ' : ' || ')
      end

      alias to_s allowance_expression

      protected

      def default(default_action)
        raise ArgumentError, "default can only be called once in access_control block" if @default_action

        unless [:allow, :deny].include? default_action
          raise ArgumentError, "invalid value for default (can be :allow or :deny)"
        end

        @default_action = default_action
      end

      def allow(*args)
        @current_rule = :allow
        _parse_and_add_rule(*args)
      end

      def deny(*args)
        @current_rule = :deny
        _parse_and_add_rule(*args)
      end

      def actions(*args, &block)
        raise ArgumentError, "actions should receive at least 1 action as argument" if args.size < 1

        subsidiary = self.class.new(*@original_args)

        class <<subsidiary
          def actions(*args)
            raise ArgumentError, "You cannot use actions inside another actions block"
          end

          def default(*args)
            raise ArgumentError, "You cannot use default inside an actions block"
          end

          def _set_action_clause(only, except)
            raise ArgumentError, "You cannot use :only (:to) or :except inside actions block" if only || except
          end
        end

        subsidiary.acl_block!(&block)
        action_check = _action_check_expression(args)
        squash = lambda { |rules| action_check + ' && ' + _either_of(rules) }

        @allows << squash.call(subsidiary.allows) if subsidiary.allows.size > 0
        @denys  << squash.call(subsidiary.denys)  if subsidiary.denys.size > 0
      end

      alias action actions

      def logged_in; false end
      def anonymous; nil   end
      def all;       true  end

      alias everyone all
      alias everybody all
      alias anyone all

      def _permitted_allow_deny_option!(key)
        raise ArgumentError, "#{key} is not a valid option" unless [:to, :only, :except, :if, :unless, *VALID_PREPOSITIONS].include?(key.to_sym)
      end

      def _retrieve_only options
        only = [ options.delete(:only) ].flatten.compact
        only |= [ options.delete(:to) ].flatten.compact
        only if only.present?
      end

      def _parse_and_add_rule(*args)
        options = args.extract_options!
        options.keys.each { |key| _permitted_allow_deny_option!(key) }

        _set_action_clause( _retrieve_only(options), options.delete(:except))

        object_s = _role_object_s(options)

        role_checks = args.map do |who|
          case who
          when anonymous then "#{_subject_ref}.nil?"
          when logged_in then "!#{_subject_ref}.nil?"
          when all       then "true"
          else
            "!#{_subject_ref}.nil? && #{_subject_ref}.has_role?('#{who}'#{object_s})"
          end
        end

        [:if, :unless].each do |cond|
          val = options[cond]
          raise ArgumentError, "#{cond} option must be a Symbol" if val && !val.is_a?(Symbol)
        end

        condition = [
          (_method_ref(options[:if]) if options[:if]),
          ("!#{_method_ref(options[:unless])}" if options[:unless])
        ].compact.join(' && ')

        condition = nil if condition.blank?

        _add_rule(case role_checks.size
                  when 0
                    raise ArgumentError, "allow/deny should have at least 1 argument"
                  when 1 then role_checks.first
                  else
                    _either_of(role_checks)
                  end, condition)
      end

      def _either_of(exprs)
        clause = exprs.map { |expr| "(#{expr})" }.join(' || ')
        return "(#{clause})"
      end

      def _add_rule(what, condition)
        anded = [what] + [@action_clause, condition].compact
        anded[0] = "(#{anded[0]})" if anded.size > 1

        (@current_rule == :allow ? @allows : @denys) << anded.join(' && ')
      end

      def _set_action_clause(only, except)
        raise ArgumentError, "both :only (:to) and :except cannot be specified in the rule" if only && except

        @action_clause  = nil
        action_list     = only || except
        return unless action_list

        expr = _action_check_expression(action_list)
        @action_clause = only ? "#{expr}" : "!#{expr}"
      end

      def _action_check_expression(action_list)
        unless action_list.is_a?(Array)
          action_list = [ action_list.to_s ]
        end

        case action_list.size
        when 0 then "true"
        when 1 then "(#{_action_ref} == '#{action_list.first}')"
        else
          set_of_actions = "Set.new([" + action_list.map { |act| "'#{act}'"}.join(',')  + "])"

          "#{set_of_actions}.include?(#{_action_ref})"
        end
      end

      def _role_object_s(options)
        object = _by_preposition options

        case object
        when Class  then ", #{object}"
        when Symbol then ", #{_object_ref object}"
        when nil    then ""
        else
          raise ArgumentError, "object specified by preposition can only be a Class or a Symbol"
        end
      end

      def _subject_ref
        raise
      end

      def _object_ref(object)
        raise
      end

      def _action_ref
        raise
      end

      def _method_ref(method)
        raise
      end
    end
  end
end