Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/declarative_policy/rule.rb')
-rw-r--r--lib/declarative_policy/rule.rb301
1 files changed, 301 insertions, 0 deletions
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
new file mode 100644
index 00000000000..bfcec241489
--- /dev/null
+++ b/lib/declarative_policy/rule.rb
@@ -0,0 +1,301 @@
+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(*a)
+ new(*a).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 nil 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 nil 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 nil 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)
+ passes = @rules.map { |r| r.cached_pass?(context) }
+ return false if passes.any? { |p| p == false }
+ return true if passes.all? { |p| p == true }
+
+ nil
+ 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)
+ passes = @rules.map { |r| r.cached_pass?(context) }
+ return true if passes.any? { |p| p == true }
+ return false if passes.all? { |p| p == false }
+
+ nil
+ 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