class DeclarativePolicy::Base

Attributes

subject[R]

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.

user[R]

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.

Public Class Methods

ability_map() click to toggle source

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.

# File lib/declarative_policy/base.rb, line 42
def ability_map
  if self == Base
    own_ability_map
  else
    superclass.ability_map.merge(own_ability_map)
  end
end
condition(name, opts = {}, &value) click to toggle source

Declares a condition. It gets stored in `own_conditions`, and generates a query method based on the condition's name.

# File lib/declarative_policy/base.rb, line 178
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)

  own_conditions[name] = condition

  define_method(:"#{name}?") { condition(name).pass? }
end
conditions() click to toggle source

an inheritable map of conditions, by name

# File lib/declarative_policy/base.rb, line 55
def conditions
  if self == Base
    own_conditions
  else
    superclass.conditions.merge(own_conditions)
  end
end
configuration_for(ability) click to toggle source

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.

# File lib/declarative_policy/base.rb, line 99
def configuration_for(ability)
  ability_map.actions(ability) + global_actions
end
delegate(name = nil, &delegation_block) click to toggle source

declaration methods ###

# File lib/declarative_policy/base.rb, line 105
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

  # rubocop: disable GitlabSecurity/PublicSend
  delegation_block = proc { @subject.__send__(name) } if delegation_block.nil?
  # rubocop: enable GitlabSecurity/PublicSend

  own_delegations[name] = delegation_block
end
delegations() click to toggle source

an inheritable map of delegations, indexed by name (which may be autogenerated)

# File lib/declarative_policy/base.rb, line 84
def delegations
  if self == Base
    own_delegations
  else
    superclass.delegations.merge(own_delegations)
  end
end
desc(description) click to toggle source

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.

# File lib/declarative_policy/base.rb, line 160
def desc(description)
  last_options[:description] = description
end
enable_when(abilities, rule) click to toggle source

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.

# File lib/declarative_policy/base.rb, line 196
def enable_when(abilities, rule)
  abilities.each { |a| own_ability_map.enable(a, rule) }
end
global_actions() click to toggle source

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.

# File lib/declarative_policy/base.rb, line 70
def global_actions
  if self == Base
    own_global_actions
  else
    superclass.global_actions + own_global_actions
  end
end
last_options() click to toggle source

A hash in which to store calls to `desc` and `with_scope`, etc.

# File lib/declarative_policy/base.rb, line 148
def last_options
  @last_options ||= {}.with_indifferent_access
end
last_options!() click to toggle source

retrieve and zero out the previously set options (used in .condition)

# File lib/declarative_policy/base.rb, line 153
def last_options!
  last_options.tap { @last_options = nil }
end
new(user, subject, opts = {}) click to toggle source
# File lib/declarative_policy/base.rb, line 219
def initialize(user, subject, opts = {})
  @user = user
  @subject = subject
  @cache = opts[:cache] || {}
end
overrides(*names) click to toggle source

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
# File lib/declarative_policy/base.rb, line 133
def overrides(*names)
  @overrides ||= [].to_set
  @overrides.merge(names)
end
own_ability_map() click to toggle source
# File lib/declarative_policy/base.rb, line 50
def own_ability_map
  @own_ability_map ||= AbilityMap.new
end
own_conditions() click to toggle source
# File lib/declarative_policy/base.rb, line 63
def own_conditions
  @own_conditions ||= {}
end
own_delegations() click to toggle source
# File lib/declarative_policy/base.rb, line 92
def own_delegations
  @own_delegations ||= {}
end
own_global_actions() click to toggle source
# File lib/declarative_policy/base.rb, line 78
def own_global_actions
  @own_global_actions ||= []
end
prevent_all_when(rule) click to toggle source

we store global prevents (from `prevent_all`) separately, so that they can be combined into every decision made.

# File lib/declarative_policy/base.rb, line 206
def prevent_all_when(rule)
  own_global_actions << [:prevent, rule]
end
prevent_when(abilities, rule) click to toggle source
# File lib/declarative_policy/base.rb, line 200
def prevent_when(abilities, rule)
  abilities.each { |a| own_ability_map.prevent(a, rule) }
end
rule(&block) click to toggle source

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.

# File lib/declarative_policy/base.rb, line 142
def rule(&block)
  rule = RuleDsl.new(self).instance_eval(&block)
  PolicyDsl.new(self, rule)
end
with_options(opts = {}) click to toggle source
# File lib/declarative_policy/base.rb, line 164
def with_options(opts = {})
  last_options.merge!(opts)
end
with_scope(scope) click to toggle source
# File lib/declarative_policy/base.rb, line 168
def with_scope(scope)
  with_options scope: scope
end
with_score(score) click to toggle source
# File lib/declarative_policy/base.rb, line 172
def with_score(score)
  with_options score: score
end

Public Instance Methods

allowed?(*abilities) click to toggle source

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.

# File lib/declarative_policy/base.rb, line 235
def allowed?(*abilities)
  abilities.all? { |a| runner(a).pass? }
end
banned?() click to toggle source

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.

# File lib/declarative_policy/base.rb, line 322
def banned?
  global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) }
  !Runner.new(global_steps).pass?
end
cache(key) { || ... } click to toggle source

Helpers for caching. Used by ManifestCondition in performing condition computation.

NOTE we can't use ||= here because the value might be the boolean `false`

# File lib/declarative_policy/base.rb, line 297
def cache(key)
  return @cache[key] if cached?(key)

  @cache[key] = yield
end
cached?(key) click to toggle source
# File lib/declarative_policy/base.rb, line 303
def cached?(key)
  !@cache[key].nil?
end
can?(ability, new_subject = :_self) click to toggle source

helper for checking abilities on this and other subjects for the current user.

# File lib/declarative_policy/base.rb, line 227
def can?(ability, new_subject = :_self)
  return allowed?(ability) if new_subject == :_self

  policy_for(new_subject).allowed?(ability)
end
condition(name) click to toggle source

returns a ManifestCondition capable of computing itself. The computation will use our own @cache.

# File lib/declarative_policy/base.rb, line 309
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
debug(ability, *args) click to toggle source

computes the given ability and prints a helpful debugging output showing which

# File lib/declarative_policy/base.rb, line 246
def debug(ability, *args)
  runner(ability).debug(*args)
end
delegated_policies() click to toggle source

A list of other policies that we've delegated to (see `Base.delegate`)

# File lib/declarative_policy/base.rb, line 328
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
disallowed?(*abilities) click to toggle source

The inverse of allowed?, used mainly in specs.

# File lib/declarative_policy/base.rb, line 240
def disallowed?(*abilities)
  abilities.all? { |a| !runner(a).pass? }
end
inspect() click to toggle source
# File lib/declarative_policy/base.rb, line 269
def inspect
  "#<#{self.class.name} #{repr}>"
end
policy_for(other_subject) click to toggle source
# File lib/declarative_policy/base.rb, line 339
def policy_for(other_subject)
  DeclarativePolicy.policy_for(@user, other_subject, cache: @cache)
end
repr() click to toggle source
# File lib/declarative_policy/base.rb, line 256
def repr
  subject_repr =
    if @subject.respond_to?(:id)
      "#{@subject.class.name}/#{@subject.id}"
    else
      @subject.inspect
    end

  user_repr = @user.try(:to_reference) || '<anonymous>'

  "(#{user_repr} : #{subject_repr})"
end
runner(ability) click to toggle source

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.

# File lib/declarative_policy/base.rb, line 277
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

Protected Instance Methods

own_steps(ability) click to toggle source

constructs steps that come from this policy and not from any delegations

# File lib/declarative_policy/base.rb, line 346
def own_steps(ability)
  rules = self.class.configuration_for(ability)
  rules.map { |(action, rule)| Step.new(self, rule, action) }
end