diff options
Diffstat (limited to 'lib/declarative_policy/base.rb')
-rw-r--r-- | lib/declarative_policy/base.rb | 354 |
1 files changed, 0 insertions, 354 deletions
diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb deleted file mode 100644 index 49cbdd2aeb4..00000000000 --- a/lib/declarative_policy/base.rb +++ /dev/null @@ -1,354 +0,0 @@ -# frozen_string_literal: true - -module DeclarativePolicy - class Base - # A map of ability => list of rules together with :enable - # or :prevent actions. Used to look up which rules apply to - # a given ability. See Base.ability_map - class AbilityMap - attr_reader :map - def initialize(map = {}) - @map = map - end - - # This merge behavior is different than regular hashes - if both - # share a key, the values at that key are concatenated, rather than - # overridden. - def merge(other) - conflict_proc = proc { |key, my_val, other_val| my_val + other_val } - AbilityMap.new(@map.merge(other.map, &conflict_proc)) - end - - def actions(key) - @map[key] ||= [] - end - - def enable(key, rule) - actions(key) << [:enable, rule] - end - - def prevent(key, rule) - actions(key) << [:prevent, rule] - end - end - - class << self - # The `own_ability_map` vs `ability_map` distinction is used so that - # the data structure is properly inherited - with subclasses recursively - # merging their parent class. - # - # This pattern is also used for conditions, global_actions, and delegations. - def ability_map - if self == Base - own_ability_map - else - superclass.ability_map.merge(own_ability_map) - end - end - - def own_ability_map - @own_ability_map ||= AbilityMap.new - end - - # an inheritable map of conditions, by name - def conditions - if self == Base - own_conditions - else - superclass.conditions.merge(own_conditions) - end - end - - def own_conditions - @own_conditions ||= {} - end - - # a list of global actions, generated by `prevent_all`. these aren't - # stored in `ability_map` because they aren't indexed by a particular - # ability. - def global_actions - if self == Base - own_global_actions - else - superclass.global_actions + own_global_actions - end - end - - def own_global_actions - @own_global_actions ||= [] - end - - # an inheritable map of delegations, indexed by name (which may be - # autogenerated) - def delegations - if self == Base - own_delegations - else - superclass.delegations.merge(own_delegations) - end - end - - def own_delegations - @own_delegations ||= {} - end - - # all the [rule, action] pairs that apply to a particular ability. - # we combine the specific ones looked up in ability_map with the global - # ones. - def configuration_for(ability) - ability_map.actions(ability) + global_actions - end - - ### declaration methods ### - - def delegate(name = nil, &delegation_block) - if name.nil? - @delegate_name_counter ||= 0 - @delegate_name_counter += 1 - name = :"anonymous_#{@delegate_name_counter}" - end - - name = name.to_sym - - if delegation_block.nil? - delegation_block = proc { @subject.__send__(name) } # rubocop:disable GitlabSecurity/PublicSend - end - - own_delegations[name] = delegation_block - end - - # Declare that the given abilities should not be read from delegates. - # - # This is useful if you have an ability that you want to define - # differently in a policy than in a delegated policy, but still want to - # delegate all other abilities. - # - # example: - # - # delegate { @subect.parent } - # - # overrides :drive_car, :watch_tv - # - def overrides(*names) - @overrides ||= [].to_set - @overrides.merge(names) - end - - # Declares a rule, constructed using RuleDsl, and returns - # a PolicyDsl which is used for registering the rule with - # this class. PolicyDsl will call back into Base.enable_when, - # Base.prevent_when, and Base.prevent_all_when. - def rule(&block) - rule = RuleDsl.new(self).instance_eval(&block) - PolicyDsl.new(self, rule) - end - - # A hash in which to store calls to `desc` and `with_scope`, etc. - def last_options - @last_options ||= {}.with_indifferent_access - end - - # retrieve and zero out the previously set options (used in .condition) - def last_options! - last_options.tap { @last_options = nil } - end - - # Declare a description for the following condition. Currently unused, - # but opens the potential for explaining to users why they were or were - # not able to do something. - def desc(description) - last_options[:description] = description - end - - def with_options(opts = {}) - last_options.merge!(opts) - end - - def with_scope(scope) - with_options scope: scope - end - - def with_score(score) - with_options score: score - end - - # Declares a condition. It gets stored in `own_conditions`, and generates - # a query method based on the condition's name. - def condition(name, opts = {}, &value) - name = name.to_sym - - opts = last_options!.merge(opts) - opts[:context_key] ||= self.name - - condition = Condition.new(name, opts, &value) - - self.own_conditions[name] = condition - - define_method(:"#{name}?") { condition(name).pass? } - end - - # These next three methods are mainly called from PolicyDsl, - # and are responsible for "inverting" the relationship between - # an ability and a rule. We store in `ability_map` a map of - # abilities to rules that affect them, together with a - # symbol indicating :prevent or :enable. - def enable_when(abilities, rule) - abilities.each { |a| own_ability_map.enable(a, rule) } - end - - def prevent_when(abilities, rule) - abilities.each { |a| own_ability_map.prevent(a, rule) } - end - - # we store global prevents (from `prevent_all`) separately, - # so that they can be combined into every decision made. - def prevent_all_when(rule) - own_global_actions << [:prevent, rule] - end - end - - # A policy object contains a specific user and subject on which - # to compute abilities. For this reason it's sometimes called - # "context" within the framework. - # - # It also stores a reference to the cache, so it can be used - # to cache computations by e.g. ManifestCondition. - attr_reader :user, :subject - def initialize(user, subject, opts = {}) - @user = user - @subject = subject - @cache = opts[:cache] || {} - end - - # helper for checking abilities on this and other subjects - # for the current user. - def can?(ability, new_subject = :_self) - return allowed?(ability) if new_subject == :_self - - policy_for(new_subject).allowed?(ability) - end - - # This is the main entry point for permission checks. It constructs - # or looks up a Runner for the given ability and asks it if it passes. - def allowed?(*abilities) - abilities.all? { |a| runner(a).pass? } - end - - # The inverse of #allowed?, used mainly in specs. - def disallowed?(*abilities) - abilities.all? { |a| !runner(a).pass? } - end - - # computes the given ability and prints a helpful debugging output - # showing which - def debug(ability, *args) - runner(ability).debug(*args) - end - - desc "Unknown user" - condition(:anonymous, scope: :user, score: 0) { @user.nil? } - - desc "By default" - condition(:default, scope: :global, score: 0) { true } - - def repr - subject_repr = - if @subject.respond_to?(:id) - "#{@subject.class.name}/#{@subject.id}" - else - @subject.inspect - end - - user_repr = - if @user - @user.to_reference - else - "<anonymous>" - end - - "(#{user_repr} : #{subject_repr})" - end - - def inspect - "#<#{self.class.name} #{repr}>" - end - - # returns a Runner for the given ability, capable of computing whether - # the ability is allowed. Runners are cached on the policy (which itself - # is cached on @cache), and caches its result. This is how we perform caching - # at the ability level. - def runner(ability) - ability = ability.to_sym - @runners ||= {} - @runners[ability] ||= - begin - own_runner = Runner.new(own_steps(ability)) - if self.class.overrides.include?(ability) - own_runner - else - delegated_runners = delegated_policies.values.compact.map { |p| p.runner(ability) } - delegated_runners.inject(own_runner, &:merge_runner) - end - end - end - - # Helpers for caching. Used by ManifestCondition in performing condition - # computation. - # - # NOTE we can't use ||= here because the value might be the - # boolean `false` - def cache(key) - return @cache[key] if cached?(key) - - @cache[key] = yield - end - - def cached?(key) - !@cache[key].nil? - end - - # returns a ManifestCondition capable of computing itself. The computation - # will use our own @cache. - def condition(name) - name = name.to_sym - @_conditions ||= {} - @_conditions[name] ||= - begin - raise "invalid condition #{name}" unless self.class.conditions.key?(name) - - ManifestCondition.new(self.class.conditions[name], self) - end - end - - # used in specs - returns true if there is no possible way for any action - # to be allowed, determined only by the global :prevent_all rules. - def banned? - global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) } - !Runner.new(global_steps).pass? - end - - # A list of other policies that we've delegated to (see `Base.delegate`) - def delegated_policies - @delegated_policies ||= self.class.delegations.transform_values do |block| - new_subject = instance_eval(&block) - - # never delegate to nil, as that would immediately prevent_all - next if new_subject.nil? - - policy_for(new_subject) - end - end - - def policy_for(other_subject) - DeclarativePolicy.policy_for(@user, other_subject, cache: @cache) - end - - protected - - # constructs steps that come from this policy and not from any delegations - def own_steps(ability) - rules = self.class.configuration_for(ability) - rules.map { |(action, rule)| Step.new(self, rule, action) } - end - end -end |