Class: DeclarativePolicy::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/declarative_policy/base.rb

Direct Known Subclasses

NilPolicy

Defined Under Namespace

Classes: AbilityMap, Options

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user, subject, opts = {}) ⇒ Base

Returns a new instance of Base.



241
242
243
244
245
# File 'lib/declarative_policy/base.rb', line 241

def initialize(user, subject, opts = {})
  @user = user
  @subject = subject
  @cache = opts[:cache] || {}
end

Instance Attribute Details

#subjectObject (readonly)

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.



239
240
241
# File 'lib/declarative_policy/base.rb', line 239

def subject
  @subject
end

#userObject (readonly)

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.



239
240
241
# File 'lib/declarative_policy/base.rb', line 239

def user
  @user
end

Class Method Details

.ability_mapObject

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.



60
61
62
63
64
65
66
# File 'lib/declarative_policy/base.rb', line 60

def ability_map
  if self == Base
    own_ability_map
  else
    superclass.ability_map.merge(own_ability_map)
  end
end

.condition(condition_name, opts = {}, &value) ⇒ Object

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



193
194
195
196
197
198
199
200
201
# File 'lib/declarative_policy/base.rb', line 193

def condition(condition_name, opts = {}, &value)
  condition_name = condition_name.to_sym

  condition = Condition.new(condition_name, condition_options(opts), &value)

  own_conditions[condition_name] = condition

  define_method(:"#{condition_name}?") { condition(condition_name).pass? }
end

.conditionsObject

an inheritable map of conditions, by name



73
74
75
76
77
78
79
# File 'lib/declarative_policy/base.rb', line 73

def conditions
  if self == Base
    own_conditions
  else
    superclass.conditions.merge(own_conditions)
  end
end

.configuration_for(ability) ⇒ Object

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.



117
118
119
# File 'lib/declarative_policy/base.rb', line 117

def configuration_for(ability)
  ability_map.actions(ability) + global_actions
end

.delegate(name = nil, &delegation_block) ⇒ Object

declaration methods ###



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/declarative_policy/base.rb', line 123

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

.delegationsObject

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



102
103
104
105
106
107
108
# File 'lib/declarative_policy/base.rb', line 102

def delegations
  if self == Base
    own_delegations
  else
    superclass.delegations.merge(own_delegations)
  end
end

.desc(description) ⇒ Object

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.



177
178
179
# File 'lib/declarative_policy/base.rb', line 177

def desc(description)
  with_options description: description
end

.enable_when(abilities, rule) ⇒ Object

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.



208
209
210
# File 'lib/declarative_policy/base.rb', line 208

def enable_when(abilities, rule)
  abilities.each { |a| own_ability_map.enable(a, rule) }
end

.global_actionsObject

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.



88
89
90
91
92
93
94
# File 'lib/declarative_policy/base.rb', line 88

def global_actions
  if self == Base
    own_global_actions
  else
    superclass.global_actions + own_global_actions
  end
end

.last_optionsObject

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



166
167
168
# File 'lib/declarative_policy/base.rb', line 166

def last_options
  @last_options ||= Options.new
end

.overrides(*names) ⇒ Object

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


151
152
153
154
# File 'lib/declarative_policy/base.rb', line 151

def overrides(*names)
  @overrides ||= [].to_set
  @overrides.merge(names)
end

.own_ability_mapObject



68
69
70
# File 'lib/declarative_policy/base.rb', line 68

def own_ability_map
  @own_ability_map ||= AbilityMap.new
end

.own_conditionsObject



81
82
83
# File 'lib/declarative_policy/base.rb', line 81

def own_conditions
  @own_conditions ||= {}
end

.own_delegationsObject



110
111
112
# File 'lib/declarative_policy/base.rb', line 110

def own_delegations
  @own_delegations ||= {}
end

.own_global_actionsObject



96
97
98
# File 'lib/declarative_policy/base.rb', line 96

def own_global_actions
  @own_global_actions ||= []
end

.prevent_all_when(rule) ⇒ Object

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



218
219
220
# File 'lib/declarative_policy/base.rb', line 218

def prevent_all_when(rule)
  own_global_actions << [:prevent, rule]
end

.prevent_when(abilities, rule) ⇒ Object



212
213
214
# File 'lib/declarative_policy/base.rb', line 212

def prevent_when(abilities, rule)
  abilities.each { |a| own_ability_map.prevent(a, rule) }
end

.rule(&block) ⇒ Object

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.



160
161
162
163
# File 'lib/declarative_policy/base.rb', line 160

def rule(&block)
  rule = RuleDsl.new(self).instance_eval(&block)
  PolicyDsl.new(self, rule)
end

.with_options(opts = {}) ⇒ Object



170
171
172
# File 'lib/declarative_policy/base.rb', line 170

def with_options(opts = {})
  last_options.to_h.merge!(opts.to_h)
end

.with_scope(scope) ⇒ Object

Declare a scope for the following condition.



182
183
184
# File 'lib/declarative_policy/base.rb', line 182

def with_scope(scope)
  with_options scope: scope
end

.with_score(score) ⇒ Object

Declare a score for the following condition.



187
188
189
# File 'lib/declarative_policy/base.rb', line 187

def with_score(score)
  with_options score: score
end

Instance Method Details

#allowed?(*abilities) ⇒ Boolean

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.

Returns:

  • (Boolean)


257
258
259
# File 'lib/declarative_policy/base.rb', line 257

def allowed?(*abilities)
  abilities.all? { |a| runner(a).pass? }
end

#banned?Boolean

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.

Returns:

  • (Boolean)


354
355
356
357
# File 'lib/declarative_policy/base.rb', line 354

def banned?
  global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) }
  !Runner.new(global_steps).pass?
end

#cache(key) ⇒ Object

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

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



329
330
331
332
333
# File 'lib/declarative_policy/base.rb', line 329

def cache(key)
  return @cache[key] if cached?(key)

  @cache[key] = yield
end

#cached?(key) ⇒ Boolean

Returns:

  • (Boolean)


335
336
337
# File 'lib/declarative_policy/base.rb', line 335

def cached?(key)
  !@cache[key].nil?
end

#can?(ability, new_subject = :_self) ⇒ Boolean

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

Returns:

  • (Boolean)


249
250
251
252
253
# File 'lib/declarative_policy/base.rb', line 249

def can?(ability, new_subject = :_self)
  return allowed?(ability) if new_subject == :_self

  policy_for(new_subject).allowed?(ability)
end

#condition(name) ⇒ Object

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



341
342
343
344
345
346
347
348
349
350
# File 'lib/declarative_policy/base.rb', line 341

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) ⇒ Object

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



268
269
270
# File 'lib/declarative_policy/base.rb', line 268

def debug(ability, *args)
  runner(ability).debug(*args)
end

#delegated_policiesObject

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



360
361
362
363
364
365
366
367
368
369
# File 'lib/declarative_policy/base.rb', line 360

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) ⇒ Boolean

The inverse of #allowed?, used mainly in specs.

Returns:

  • (Boolean)


262
263
264
# File 'lib/declarative_policy/base.rb', line 262

def disallowed?(*abilities)
  abilities.all? { |a| !runner(a).pass? }
end

#identify_subjectObject



290
291
292
293
294
295
296
# File 'lib/declarative_policy/base.rb', line 290

def identify_subject
  if @subject.respond_to?(:id)
    "#{@subject.class.name}/#{@subject.id}"
  else
    @subject.inspect
  end
end

#identify_userObject



282
283
284
285
286
287
288
# File 'lib/declarative_policy/base.rb', line 282

def identify_user
  return '<anonymous>' unless @user

  @user.to_reference
rescue NoMethodError
  "<#{@user.class}: #{@user.object_id}>"
end

#inspectObject



298
299
300
# File 'lib/declarative_policy/base.rb', line 298

def inspect
  "#<#{self.class.name} #{repr}>"
end

#policy_for(other_subject) ⇒ Object



371
372
373
# File 'lib/declarative_policy/base.rb', line 371

def policy_for(other_subject)
  DeclarativePolicy.policy_for(@user, other_subject, cache: @cache)
end

#reprObject



278
279
280
# File 'lib/declarative_policy/base.rb', line 278

def repr
  "(#{identify_user} : #{identify_subject})"
end

#runner(ability) ⇒ Object

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.



306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/declarative_policy/base.rb', line 306

def runner(ability)
  ability = ability.to_sym
  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.reduce(own_runner, &:merge_runner)
      end
    end
end

#runnersObject



320
321
322
# File 'lib/declarative_policy/base.rb', line 320

def runners
  @runners ||= {}
end