Commit fa1ac789 authored by Alex Kalderimis's avatar Alex Kalderimis

Use published declarative_policy gem

This removes the `declarative_policy` internal libary, replacing
it with the now extracted gitlab-org/declarative-policy> library.
parent da0ce505
......@@ -23,6 +23,9 @@ gem 'grape-path-helpers', '~> 1.6.1'
gem 'faraday', '~> 1.0'
gem 'marginalia', '~> 1.10.0'
# Authorization
gem 'declarative_policy', '~> 1.0.0'
# Authentication libraries
gem 'devise', '~> 4.7.2'
gem 'bcrypt', '~> 3.1', '>= 3.1.14'
......
......@@ -244,6 +244,7 @@ GEM
html-pipeline
declarative (0.0.20)
declarative-option (0.1.0)
declarative_policy (1.0.0)
default_value_for (3.4.0)
activerecord (>= 3.2.0, < 7.0)
deprecation_toolkit (1.5.1)
......@@ -1383,6 +1384,7 @@ DEPENDENCIES
crystalball (~> 0.7.0)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.3.1)
declarative_policy (~> 1.0.0)
default_value_for (~> 3.4.0)
deprecation_toolkit (~> 1.5.1)
derailed_benchmarks
......
# frozen_string_literal: true
class NilPolicy < BasePolicy
rule { default }.prevent_all
DeclarativePolicy.configure do
named_policy :global, ::GlobalPolicy
end
# frozen_string_literal: true
require_dependency 'declarative_policy/cache'
require_dependency 'declarative_policy/condition'
require_dependency 'declarative_policy/delegate_dsl'
require_dependency 'declarative_policy/policy_dsl'
require_dependency 'declarative_policy/rule_dsl'
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
extend PreferredScope
CLASS_CACHE_MUTEX = Mutex.new
CLASS_CACHE_IVAR = :@__DeclarativePolicy_CLASS_CACHE
class << self
def policy_for(user, subject, opts = {})
cache = opts[:cache] || {}
key = Cache.policy_key(user, subject)
cache[key] ||=
# to avoid deadlocks in multi-threaded environment when
# autoloading is enabled, we allow concurrent loads,
# https://gitlab.com/gitlab-org/gitlab-foss/issues/48263
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
class_for(subject).new(user, subject, opts)
end
end
def class_for(subject)
return GlobalPolicy if subject == :global
return NilPolicy if subject.nil?
subject = find_delegate(subject)
policy_class = class_for_class(subject.class)
raise "no policy for #{subject.class.name}" if policy_class.nil?
policy_class
end
def has_policy?(subject)
!class_for_class(subject.class).nil?
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
subject_class.instance_variable_get(CLASS_CACHE_IVAR)
end
def compute_class_for_class(subject_class)
if subject_class.respond_to?(:declarative_policy_class)
return subject_class.declarative_policy_class.constantize
end
subject_class.ancestors.each do |klass|
name = klass.name
next unless name
begin
policy_class = "#{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
nil
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)
seen << subject.object_id
subject = subject.declarative_policy_delegate
end
subject
end
end
end
This diff is collapsed.
# frozen_string_literal: true
module DeclarativePolicy
module Cache
class << self
def user_key(user)
return '<anonymous>' if user.nil?
id_for(user)
end
def policy_key(user, subject)
u = user_key(user)
s = subject_key(subject)
"/dp/policy/#{u}/#{s}"
end
def subject_key(subject)
return '<nil>' if subject.nil?
return subject.inspect if subject.is_a?(Symbol)
"#{subject.class.name}:#{id_for(subject)}"
end
private
def id_for(obj)
id =
begin
obj.id
rescue NoMethodError
nil
end
id || "##{obj.object_id}"
end
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
# A Condition is the data structure that is created by the
# `condition` declaration on DeclarativePolicy::Base. It is
# more or less just a struct of the data passed to that
# declaration. It holds on to the block to be instance_eval'd
# on a context (instance of Base) later, via #compute.
class Condition
attr_reader :name, :description, :scope
attr_reader :manual_score
attr_reader :context_key
def initialize(name, opts = {}, &compute)
@name = name
@compute = compute
@scope = opts.fetch(:scope, :normal)
@description = opts.delete(:description)
@context_key = opts[:context_key]
@manual_score = opts.fetch(:score, nil)
end
def compute(context)
!!context.instance_eval(&@compute)
end
def key
"#{@context_key}/#{@name}"
end
end
# In contrast to a Condition, a ManifestCondition contains
# a Condition and a context object, and is capable of calculating
# a result itself. This is the return value of Base#condition.
class ManifestCondition
def initialize(condition, context)
@condition = condition
@context = context
end
# The main entry point - does this condition pass? We reach into
# the context's cache here so that we can share in the global
# cache (often RequestStore or similar).
def pass?
@context.cache(cache_key) { @condition.compute(@context) }
end
# Whether we've already computed this condition.
def cached?
@context.cached?(cache_key)
end
# This is used to score Rule::Condition. See Rule::Condition#score
# and Runner#steps_by_score for how scores are used.
#
# The number here is intended to represent, abstractly, how
# expensive it would be to calculate this condition.
#
# See #cache_key for info about @condition.scope.
def score
# If we've been cached, no computation is necessary.
return 0 if cached?
# Use the override from condition(score: ...) if present
return @condition.manual_score if @condition.manual_score
# Global scope rules are cheap due to max cache sharing
return 2 if @condition.scope == :global
# "Normal" rules can't share caches with any other policies
return 16 if @condition.scope == :normal
# otherwise, we're :user or :subject scope, so it's 4 if
# the caller has declared a preference
return 4 if @condition.scope == DeclarativePolicy.preferred_scope
# and 8 for all other :user or :subject scope conditions.
8
end
private
# This method controls the caching for the condition. This is where
# the condition(scope: ...) option comes into play. Notice that
# depending on the scope, we may cache only by the user or only by
# the subject, resulting in sharing across different policy objects.
def cache_key
@cache_key ||=
case @condition.scope
when :normal then "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}"
when :user then "/dp/condition/#{@condition.key}/#{user_key}"
when :subject then "/dp/condition/#{@condition.key}/#{subject_key}"
when :global then "/dp/condition/#{@condition.key}"
else raise 'invalid scope'
end
end
def user_key
Cache.user_key(@context.user)
end
def subject_key
Cache.subject_key(@context.subject)
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
# Used when the name of a delegate is mentioned in
# the rule DSL.
class DelegateDsl
def initialize(rule_dsl, delegate_name)
@rule_dsl = rule_dsl
@delegate_name = delegate_name
end
def method_missing(msg, *args)
return super unless args.empty? && !block_given?
@rule_dsl.delegate(@delegate_name, msg)
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
# The return value of a rule { ... } declaration.
# Can call back to register rules with the containing
# Policy class (context_class here). See Base.rule
#
# Note that the #policy method just performs an #instance_eval,
# which is useful for multiple #enable or #prevent calls.
#
# Also provides a #method_missing proxy to the context
# class's class methods, so that helper methods can be
# defined and used in a #policy { ... } block.
class PolicyDsl
def initialize(context_class, rule)
@context_class = context_class
@rule = rule
end
def policy(&block)
instance_eval(&block)
end
def enable(*abilities)
@context_class.enable_when(abilities, @rule)
end
def prevent(*abilities)
@context_class.prevent_when(abilities, @rule)
end
def prevent_all
@context_class.prevent_all_when(@rule)
end
def method_missing(msg, *args, &block)
return super unless @context_class.respond_to?(msg)
@context_class.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(msg)
@context_class.respond_to?(msg) || super
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
module PreferredScope
PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope"
def with_preferred_scope(scope)
old_scope = Thread.current[PREFERRED_SCOPE_KEY]
Thread.current[PREFERRED_SCOPE_KEY] = scope
yield
ensure
Thread.current[PREFERRED_SCOPE_KEY] = old_scope
end
def preferred_scope
Thread.current[PREFERRED_SCOPE_KEY]
end
def user_scope(&block)
with_preferred_scope(:user, &block)
end
def subject_scope(&block)
with_preferred_scope(:subject, &block)
end
def preferred_scope=(scope)
Thread.current[PREFERRED_SCOPE_KEY] = scope
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
module Rule
# A Rule is the object that results from the `rule` declaration,
# usually built using the DSL in `RuleDsl`. It is a basic logical
# combination of building blocks, and is capable of deciding,
# given a context (instance of DeclarativePolicy::Base) whether it
# passes or not. Note that this decision doesn't by itself know
# how that affects the actual ability decision - for that, a
# `Step` is used.
class Base
def self.make(*args)
new(*args).simplify
end
# true or false whether this rule passes.
# `context` is a policy - an instance of
# DeclarativePolicy::Base.
def pass?(context)
raise 'abstract'
end
# same as #pass? except refuses to do any I/O,
# returning nil if the result is not yet cached.
# used for accurately scoring And/Or
def cached_pass?(context)
raise 'abstract'
end
# abstractly, how long would it take to compute
# this rule? lower-scored rules are tried first.
def score(context)
raise 'abstract'
end
# unwrap double negatives and nested and/or
def simplify
self
end
# convenience combination methods
def or(other)
Or.make([self, other])
end
def and(other)
And.make([self, other])
end
def negate
Not.make(self)
end
alias_method :|, :or
alias_method :&, :and
alias_method :~@, :negate
def inspect
"#<Rule #{repr}>"
end
end
# A rule that checks a condition. This is the
# type of rule that results from a basic bareword
# in the rule dsl (see RuleDsl#method_missing).
class Condition < Base
def initialize(name)
@name = name
end
# we delegate scoring to the condition. See
# ManifestCondition#score.
def score(context)
context.condition(@name).score
end
# Let the ManifestCondition from the context
# decide whether we pass.
def pass?(context)
context.condition(@name).pass?
end
# returns nil unless it's already cached
def cached_pass?(context)
condition = context.condition(@name)
return unless condition.cached?
condition.pass?
end
def description(context)
context.class.conditions[@name].description
end
def repr
@name.to_s
end
end
# A rule constructed from DelegateDsl - using a condition from a
# delegated policy.
class DelegatedCondition < Base
# Internal use only - this is rescued each time it's raised.
MissingDelegate = Class.new(StandardError)
def initialize(delegate_name, name)
@delegate_name = delegate_name
@name = name
end
def delegated_context(context)
policy = context.delegated_policies[@delegate_name]
raise MissingDelegate if policy.nil?
policy
end
def score(context)
delegated_context(context).condition(@name).score
rescue MissingDelegate
0
end
def cached_pass?(context)
condition = delegated_context(context).condition(@name)
return unless condition.cached?
condition.pass?
rescue MissingDelegate
false
end
def pass?(context)
delegated_context(context).condition(@name).pass?
rescue MissingDelegate
false
end
def repr
"#{@delegate_name}.#{@name}"
end
end
# A rule constructed from RuleDsl#can?. Computes a different ability
# on the same subject.
class Ability < Base
attr_reader :ability
def initialize(ability)
@ability = ability
end
# We ask the ability's runner for a score
def score(context)
context.runner(@ability).score
end
def pass?(context)
context.allowed?(@ability)
end
def cached_pass?(context)
runner = context.runner(@ability)
return unless runner.cached?
runner.pass?
end
def description(context)
"User can #{@ability.inspect}"
end
def repr
"can?(#{@ability.inspect})"
end
end
# Logical `and`, containing a list of rules. Only passes
# if all of them do.
class And < Base
attr_reader :rules
def initialize(rules)
@rules = rules
end
def simplify
simplified_rules = @rules.flat_map do |rule|
simplified = rule.simplify
case simplified
when And then simplified.rules
else [simplified]
end
end
And.new(simplified_rules)
end
def score(context)
return 0 unless cached_pass?(context).nil?
# note that cached rules will have score 0 anyways.
@rules.map { |r| r.score(context) }.inject(0, :+)
end
def pass?(context)
# try to find a cached answer before
# checking in order
cached = cached_pass?(context)
return cached unless cached.nil?
@rules.all? { |r| r.pass?(context) }
end
def cached_pass?(context)
@rules.each do |rule|
pass = rule.cached_pass?(context)
return pass if pass.nil? || pass == false
end
true
end
def repr
"all?(#{rules.map(&:repr).join(', ')})"
end
end
# Logical `or`. Mirrors And.
class Or < Base
attr_reader :rules
def initialize(rules)
@rules = rules
end
def pass?(context)
cached = cached_pass?(context)
return cached unless cached.nil?
@rules.any? { |r| r.pass?(context) }
end
def simplify
simplified_rules = @rules.flat_map do |rule|
simplified = rule.simplify
case simplified
when Or then simplified.rules
else [simplified]
end
end
Or.new(simplified_rules)
end
def cached_pass?(context)
@rules.each do |rule|
pass = rule.cached_pass?(context)
return pass if pass.nil? || pass == true
end
false
end
def score(context)
return 0 unless cached_pass?(context).nil?
@rules.map { |r| r.score(context) }.inject(0, :+)
end
def repr
"any?(#{@rules.map(&:repr).join(', ')})"
end
end
class Not < Base
attr_reader :rule
def initialize(rule)
@rule = rule
end
def simplify
case @rule
when And then Or.new(@rule.rules.map(&:negate)).simplify
when Or then And.new(@rule.rules.map(&:negate)).simplify
when Not then @rule.rule.simplify
else Not.new(@rule.simplify)
end
end
def pass?(context)
!@rule.pass?(context)
end
def cached_pass?(context)
case @rule.cached_pass?(context)
when nil then nil
when true then false
when false then true
end
end
def score(context)
@rule.score(context)
end
def repr
"~#{@rule.repr}"
end
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
# The DSL evaluation context inside rule { ... } blocks.
# Responsible for creating and combining Rule objects.
#
# See Base.rule
class RuleDsl
def initialize(context_class)
@context_class = context_class
end
def can?(ability)
Rule::Ability.new(ability)
end
def all?(*rules)
Rule::And.make(rules)
end
def any?(*rules)
Rule::Or.make(rules)
end
def none?(*rules)
~Rule::Or.new(rules)
end
def cond(condition)
Rule::Condition.new(condition)
end
def delegate(delegate_name, condition)
Rule::DelegatedCondition.new(delegate_name, condition)
end
def method_missing(msg, *args)
return super unless args.empty? && !block_given?
if @context_class.delegations.key?(msg)
DelegateDsl.new(self, msg)
else
cond(msg.to_sym)
end
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
class Runner
class State
def initialize
@enabled = false
@prevented = false
end
def enable!
@enabled = true
end
def enabled?
@enabled
end
def prevent!
@prevented = true
end
def prevented?
@prevented
end
def pass?
!prevented? && enabled?
end
end
# a Runner contains a list of Steps to be run.
attr_reader :steps
def initialize(steps)
@steps = steps
@state = nil
end
# We make sure only to run any given Runner once,
# and just continue to use the resulting @state
# that's left behind.
def cached?
!!@state
end
# used by Rule::Ability. See #steps_by_score
def score
return 0 if cached?
steps.map(&:score).inject(0, :+)
end
def merge_runner(other)
Runner.new(@steps + other.steps)
end
# The main entry point, called for making an ability decision.
# See #run and DeclarativePolicy::Base#can?
def pass?
run unless cached?
@state.pass?
end
# see DeclarativePolicy::Base#debug
def debug(out = $stderr)
run(out)
end
private
def flatten_steps!
@steps = @steps.flat_map { |s| s.flattened(@steps) }
end
# This method implements the semantic of "one enable and no prevents".
# It relies on #steps_by_score for the main loop, and updates @state
# with the result of the step.
def run(debug = nil)
@state = State.new
steps_by_score do |step, score|
break if !debug && @state.prevented?
passed = nil
case step.action
when :enable then
# we only check :enable actions if they have a chance of
# changing the outcome - if no other rule has enabled or
# prevented.
unless @state.enabled? || @state.prevented?
passed = step.pass?
@state.enable! if passed
end
debug << inspect_step(step, score, passed) if debug
when :prevent then
# we only check :prevent actions if the state hasn't already
# been prevented.
unless @state.prevented?
passed = step.pass?
@state.prevent! if passed
end
debug << inspect_step(step, score, passed) if debug
else raise "invalid action #{step.action.inspect}"
end
end
@state
end
# This is the core spot where all those `#score` methods matter.
# It is critical for performance to run steps in the correct order,
# so that we don't compute expensive conditions (potentially n times
# if we're called on, say, a large list of users).
#
# In order to determine the cheapest step to run next, we rely on
# Step#score, which returns a numerical rating of how expensive
# it would be to calculate - the lower the better. It would be
# easy enough to statically sort by these scores, but we can do
# a little better - the scores are cache-aware (conditions that
# are already in the cache have score 0), which means that running
# a step can actually change the scores of other steps.
#
# So! The way we sort here involves re-scoring at every step. This
# is by necessity quadratic, but most of the time the number of steps
# will be low. But just in case, if the number of steps exceeds 50,
# we print a warning and fall back to a static sort.
#
# For each step, we yield the step object along with the computed score
# for debugging purposes.
def steps_by_score
flatten_steps!
if @steps.size > 50
warn "DeclarativePolicy: large number of steps (#{steps.size}), falling back to static sort"
@steps.map { |s| [s.score, s] }.sort_by { |(score, _)| score }.each do |(score, step)|
yield step, score
end
return
end
remaining_steps = Set.new(@steps)
remaining_enablers, remaining_preventers = remaining_steps.partition(&:enable?).map { |s| Set.new(s) }
loop do
if @state.enabled?
# Once we set this, we never need to unset it, because a single
# prevent will stop this from being enabled
remaining_steps = remaining_preventers
else
# if the permission hasn't yet been enabled and we only have
# prevent steps left, we short-circuit the state here
@state.prevent! if remaining_enablers.empty?
end
return if remaining_steps.empty?
lowest_score = Float::INFINITY
next_step = nil
remaining_steps.each do |step|
score = step.score
if score < lowest_score
next_step = step
lowest_score = score
end
break if lowest_score == 0
end
[remaining_steps, remaining_enablers, remaining_preventers].each do |set|
set.delete(next_step)
end
yield next_step, lowest_score
end
end
# Formatter for debugging output.
def inspect_step(step, original_score, passed)
symbol =
case passed
when true then '+'
when false then '-'
when nil then ' '
end
"#{symbol} [#{original_score.to_i}] #{step.repr}\n"
end
end
end
# frozen_string_literal: true
module DeclarativePolicy
# This object represents one step in the runtime decision of whether
# an ability is allowed. It contains a Rule and a context (instance
# of DeclarativePolicy::Base), which contains the user, the subject,
# and the cache. It also contains an "action", which is the symbol
# :prevent or :enable.
class Step
attr_reader :context, :rule, :action
def initialize(context, rule, action)
@context = context
@rule = rule
@action = action
end
# In the flattening process, duplicate steps may be generated in the
# same rule. This allows us to eliminate those (see Runner#steps_by_score
# and note its use of a Set)
def ==(other)
@context == other.context && @rule == other.rule && @action == other.action
end
# In the runner, steps are sorted dynamically by score, so that
# we are sure to compute them in close to the optimal order.
#
# See also Rule#score, ManifestCondition#score, and Runner#steps_by_score.
def score
# we slightly prefer the preventative actions
# since they are more likely to short-circuit
case @action
when :prevent
@rule.score(@context) * (7.0 / 8)
when :enable
@rule.score(@context)
end
end
def with_action(action)
Step.new(@context, @rule, action)
end
def enable?
@action == :enable
end
def prevent?
@action == :prevent
end
# This rather complex method allows us to split rules into parts so that
# they can be sorted independently for better optimization
def flattened(roots)
case @rule
when Rule::Or
# A single `Or` step is the same as each of its elements as separate steps
@rule.rules.flat_map { |r| Step.new(@context, r, @action).flattened(roots) }
when Rule::Ability
# This looks like a weird micro-optimization but it buys us quite a lot
# in some cases. If we depend on an Ability (i.e. a `can?(...)` rule),
# and that ability *only* has :enable actions (modulo some actions that
# we already have taken care of), then its rules can be safely inlined.
steps = @context.runner(@rule.ability).steps.reject { |s| roots.include?(s) }
if steps.all?(&:enable?)
# in the case that we are a :prevent step, each inlined step becomes
# an independent :prevent, even though it was an :enable in its initial
# context.
steps.map! { |s| s.with_action(:prevent) } if prevent?
steps.flat_map { |s| s.flattened(roots) }
else
[self]
end
else
[self]
end
end
def pass?
@rule.pass?(@context)
end
def repr
"#{@action} when #{@rule.repr} (#{@context.repr})"
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require_dependency 'rspec-parameterized'
RSpec.describe 'DeclarativePolicy overrides' do
let(:foo_policy) do
Class.new(DeclarativePolicy::Base) do
condition(:foo_prop_cond) { @subject.foo_prop }
rule { foo_prop_cond }.policy do
enable :common_ability
enable :foo_prop_ability
end
end
end
let(:bar_policy) do
Class.new(DeclarativePolicy::Base) do
delegate { @subject.foo }
overrides :common_ability
condition(:bar_prop_cond) { @subject.bar_prop }
rule { bar_prop_cond }.policy do
enable :common_ability
enable :bar_prop_ability
end
rule { bar_prop_cond & can?(:foo_prop_ability) }.policy do
enable :combined_ability
end
end
end
before do
stub_const('Foo', Struct.new(:foo_prop))
stub_const('FooPolicy', foo_policy)
stub_const('Bar', Struct.new(:foo, :bar_prop))
stub_const('BarPolicy', bar_policy)
end
where(:foo_prop, :bar_prop) do
[
[true, true],
[true, false],
[false, true],
[false, false]
]
end
with_them do
let(:foo) { Foo.new(foo_prop) }
let(:bar) { Bar.new(foo, bar_prop) }
it 'determines the correct bar_prop_ability (non-delegated) permissions for bar' do
policy = DeclarativePolicy.policy_for(nil, bar)
expect(policy.allowed?(:bar_prop_ability)).to eq(bar_prop)
end
it 'determines the correct foo_prop (non-overridden) permissions for bar' do
policy = DeclarativePolicy.policy_for(nil, bar)
expect(policy.allowed?(:foo_prop_ability)).to eq(foo_prop)
end
it 'determines the correct common_ability (overridden) permissions for bar' do
policy = DeclarativePolicy.policy_for(nil, bar)
expect(policy.allowed?(:common_ability)).to eq(bar_prop)
end
it 'determines the correct common_ability permissions for foo' do
policy = DeclarativePolicy.policy_for(nil, foo)
expect(policy.allowed?(:common_ability)).to eq(foo_prop)
end
it 'allows combinations of overridden and inherited values' do
policy = DeclarativePolicy.policy_for(nil, bar)
expect(policy.allowed?(:combined_ability)).to eq(foo_prop && bar_prop)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DeclarativePolicy do
describe '.class_for' do
it 'uses declarative_policy_class if present' do
instance = Gitlab::ErrorTracking::ErrorEvent.new
expect(described_class.class_for(instance)).to eq(ErrorTracking::BasePolicy)
end
it 'infers policy class from name' do
instance = PersonalSnippet.new
expect(described_class.class_for(instance)).to eq(PersonalSnippetPolicy)
end
it 'raises error if not found' do
instance = Object.new
expect { described_class.class_for(instance) }.to raise_error('no policy for Object')
end
context 'when found policy class does not inherit base' do
before do
stub_const('Foo', Class.new)
stub_const('FooPolicy', Class.new)
end
it 'raises error if inferred class does not inherit Base' do
instance = Foo.new
expect { described_class.class_for(instance) }.to raise_error('no policy for Foo')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment