declarative_policy.rb 3.25 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
require_dependency 'declarative_policy/cache'
require_dependency 'declarative_policy/condition'
5 6 7
require_dependency 'declarative_policy/delegate_dsl'
require_dependency 'declarative_policy/policy_dsl'
require_dependency 'declarative_policy/rule_dsl'
8 9 10 11 12 13 14 15
require_dependency 'declarative_policy/preferred_scope'
require_dependency 'declarative_policy/rule'
require_dependency 'declarative_policy/runner'
require_dependency 'declarative_policy/step'

require_dependency 'declarative_policy/base'

module DeclarativePolicy
16 17 18
  CLASS_CACHE_MUTEX = Mutex.new
  CLASS_CACHE_IVAR = :@__DeclarativePolicy_CLASS_CACHE

19 20 21 22 23
  class << self
    def policy_for(user, subject, opts = {})
      cache = opts[:cache] || {}
      key = Cache.policy_key(user, subject)

24 25 26 27 28 29 30 31 32 33 34
      cache[key] ||=
        if Gitlab.rails5?
          # to avoid deadlocks in multi-threaded environment when
          # autoloading is enabled, we allow concurrent loads,
          # https://gitlab.com/gitlab-org/gitlab-ce/issues/48263
          ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
            class_for(subject).new(user, subject, opts)
          end
        else
          class_for(subject).new(user, subject, opts)
        end
35 36 37 38 39 40 41 42
    end

    def class_for(subject)
      return GlobalPolicy if subject == :global
      return NilPolicy if subject.nil?

      subject = find_delegate(subject)

43 44
      policy_class = class_for_class(subject.class)
      raise "no policy for #{subject.class.name}" if policy_class.nil?
45

46
      policy_class
47 48 49 50
    end

    def has_policy?(subject)
      !class_for_class(subject.class).nil?
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
    end

    private

    # This method is heavily cached because there are a lot of anonymous
    # modules in play in a typical rails app, and #name performs quite
    # slowly for anonymous classes and modules.
    #
    # See https://bugs.ruby-lang.org/issues/11119
    #
    # if the above bug is resolved, this caching could likely be removed.
    def class_for_class(subject_class)
      unless subject_class.instance_variable_defined?(CLASS_CACHE_IVAR)
        CLASS_CACHE_MUTEX.synchronize do
          # re-check in case of a race
          break if subject_class.instance_variable_defined?(CLASS_CACHE_IVAR)

          policy_class = compute_class_for_class(subject_class)
          subject_class.instance_variable_set(CLASS_CACHE_IVAR, policy_class)
        end
      end

73
      subject_class.instance_variable_get(CLASS_CACHE_IVAR)
74 75 76 77
    end

    def compute_class_for_class(subject_class)
      subject_class.ancestors.each do |klass|
78 79 80 81 82 83 84 85 86 87 88 89 90
        next unless klass.name

        begin
          policy_class = "#{klass.name}Policy".constantize

          # NOTE: the < operator here tests whether policy_class
          # inherits from Base. We can't use #is_a? because that
          # tests for *instances*, not *subclasses*.
          return policy_class if policy_class < Base
        rescue NameError
          nil
        end
      end
91 92

      nil
93 94 95 96 97 98 99
    end

    def find_delegate(subject)
      seen = Set.new

      while subject.respond_to?(:declarative_policy_delegate)
        raise ArgumentError, "circular delegations" if seen.include?(subject.object_id)
100

101 102 103 104 105 106 107 108
        seen << subject.object_id
        subject = subject.declarative_policy_delegate
      end

      subject
    end
  end
end