class DeclarativePolicy::Base
Attributes
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
.
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
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
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
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
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
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
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
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
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
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
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
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
# File lib/declarative_policy/base.rb, line 219 def initialize(user, subject, opts = {}) @user = user @subject = subject @cache = opts[:cache] || {} 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
# File lib/declarative_policy/base.rb, line 133 def overrides(*names) @overrides ||= [].to_set @overrides.merge(names) end
# File lib/declarative_policy/base.rb, line 50 def own_ability_map @own_ability_map ||= AbilityMap.new end
# File lib/declarative_policy/base.rb, line 63 def own_conditions @own_conditions ||= {} end
# File lib/declarative_policy/base.rb, line 92 def own_delegations @own_delegations ||= {} end
# File lib/declarative_policy/base.rb, line 78 def own_global_actions @own_global_actions ||= [] end
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
# File lib/declarative_policy/base.rb, line 200 def prevent_when(abilities, rule) abilities.each { |a| own_ability_map.prevent(a, rule) } 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
.
# File lib/declarative_policy/base.rb, line 142 def rule(&block) rule = RuleDsl.new(self).instance_eval(&block) PolicyDsl.new(self, rule) end
# File lib/declarative_policy/base.rb, line 164 def with_options(opts = {}) last_options.merge!(opts) end
# File lib/declarative_policy/base.rb, line 168 def with_scope(scope) with_options scope: scope end
# File lib/declarative_policy/base.rb, line 172 def with_score(score) with_options score: score end
Public Instance Methods
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
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
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
# File lib/declarative_policy/base.rb, line 303 def cached?(key) !@cache[key].nil? end
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
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
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
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
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
# File lib/declarative_policy/base.rb, line 269 def inspect "#<#{self.class.name} #{repr}>" end
# File lib/declarative_policy/base.rb, line 339 def policy_for(other_subject) DeclarativePolicy.policy_for(@user, other_subject, cache: @cache) end
# 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
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
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