diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 15:09:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 15:09:24 +0000 |
commit | 8bcfcd53f3e3fe8df944eea6dab02556976fd4e3 (patch) | |
tree | 6f8cfaf7442b3ab092a107e249689e9049ee4738 /lib | |
parent | 0549ffef0d4f862a7354847dd185725cc196eed0 (diff) | |
download | gitlab-ce-8bcfcd53f3e3fe8df944eea6dab02556976fd4e3.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib')
-rw-r--r-- | lib/declarative_policy.rb | 112 | ||||
-rw-r--r-- | lib/declarative_policy/base.rb | 354 | ||||
-rw-r--r-- | lib/declarative_policy/cache.rb | 39 | ||||
-rw-r--r-- | lib/declarative_policy/condition.rb | 105 | ||||
-rw-r--r-- | lib/declarative_policy/delegate_dsl.rb | 18 | ||||
-rw-r--r-- | lib/declarative_policy/policy_dsl.rb | 46 | ||||
-rw-r--r-- | lib/declarative_policy/preferred_scope.rb | 31 | ||||
-rw-r--r-- | lib/declarative_policy/rule.rb | 312 | ||||
-rw-r--r-- | lib/declarative_policy/rule_dsl.rb | 47 | ||||
-rw-r--r-- | lib/declarative_policy/runner.rb | 196 | ||||
-rw-r--r-- | lib/declarative_policy/step.rb | 88 | ||||
-rw-r--r-- | lib/gitlab/diff/highlight.rb | 8 |
12 files changed, 5 insertions, 1351 deletions
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb deleted file mode 100644 index bd1c121fe79..00000000000 --- a/lib/declarative_policy.rb +++ /dev/null @@ -1,112 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb deleted file mode 100644 index 49cbdd2aeb4..00000000000 --- a/lib/declarative_policy/base.rb +++ /dev/null @@ -1,354 +0,0 @@ -# frozen_string_literal: true - -module DeclarativePolicy - class Base - # A map of ability => list of rules together with :enable - # or :prevent actions. Used to look up which rules apply to - # a given ability. See Base.ability_map - class AbilityMap - attr_reader :map - def initialize(map = {}) - @map = map - end - - # This merge behavior is different than regular hashes - if both - # share a key, the values at that key are concatenated, rather than - # overridden. - def merge(other) - conflict_proc = proc { |key, my_val, other_val| my_val + other_val } - AbilityMap.new(@map.merge(other.map, &conflict_proc)) - end - - def actions(key) - @map[key] ||= [] - end - - def enable(key, rule) - actions(key) << [:enable, rule] - end - - def prevent(key, rule) - actions(key) << [:prevent, rule] - end - end - - class << self - # 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. - def ability_map - if self == Base - own_ability_map - else - superclass.ability_map.merge(own_ability_map) - end - end - - def own_ability_map - @own_ability_map ||= AbilityMap.new - end - - # an inheritable map of conditions, by name - def conditions - if self == Base - own_conditions - else - superclass.conditions.merge(own_conditions) - end - end - - def own_conditions - @own_conditions ||= {} - 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. - def global_actions - if self == Base - own_global_actions - else - superclass.global_actions + own_global_actions - end - end - - def own_global_actions - @own_global_actions ||= [] - end - - # an inheritable map of delegations, indexed by name (which may be - # autogenerated) - def delegations - if self == Base - own_delegations - else - superclass.delegations.merge(own_delegations) - end - end - - def own_delegations - @own_delegations ||= {} - 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. - def configuration_for(ability) - ability_map.actions(ability) + global_actions - end - - ### declaration methods ### - - 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 - - if delegation_block.nil? - delegation_block = proc { @subject.__send__(name) } # rubocop:disable GitlabSecurity/PublicSend - end - - own_delegations[name] = delegation_block - 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 - # - def overrides(*names) - @overrides ||= [].to_set - @overrides.merge(names) - 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. - def rule(&block) - rule = RuleDsl.new(self).instance_eval(&block) - PolicyDsl.new(self, rule) - end - - # A hash in which to store calls to `desc` and `with_scope`, etc. - def last_options - @last_options ||= {}.with_indifferent_access - end - - # retrieve and zero out the previously set options (used in .condition) - def last_options! - last_options.tap { @last_options = nil } - 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. - def desc(description) - last_options[:description] = description - end - - def with_options(opts = {}) - last_options.merge!(opts) - end - - def with_scope(scope) - with_options scope: scope - end - - def with_score(score) - with_options score: score - end - - # Declares a condition. It gets stored in `own_conditions`, and generates - # a query method based on the condition's name. - 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) - - self.own_conditions[name] = condition - - define_method(:"#{name}?") { condition(name).pass? } - 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. - def enable_when(abilities, rule) - abilities.each { |a| own_ability_map.enable(a, rule) } - end - - def prevent_when(abilities, rule) - abilities.each { |a| own_ability_map.prevent(a, rule) } - end - - # we store global prevents (from `prevent_all`) separately, - # so that they can be combined into every decision made. - def prevent_all_when(rule) - own_global_actions << [:prevent, rule] - end - end - - # 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. - attr_reader :user, :subject - def initialize(user, subject, opts = {}) - @user = user - @subject = subject - @cache = opts[:cache] || {} - end - - # helper for checking abilities on this and other subjects - # for the current user. - def can?(ability, new_subject = :_self) - return allowed?(ability) if new_subject == :_self - - policy_for(new_subject).allowed?(ability) - end - - # 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. - def allowed?(*abilities) - abilities.all? { |a| runner(a).pass? } - end - - # The inverse of #allowed?, used mainly in specs. - def disallowed?(*abilities) - abilities.all? { |a| !runner(a).pass? } - end - - # computes the given ability and prints a helpful debugging output - # showing which - def debug(ability, *args) - runner(ability).debug(*args) - end - - desc "Unknown user" - condition(:anonymous, scope: :user, score: 0) { @user.nil? } - - desc "By default" - condition(:default, scope: :global, score: 0) { true } - - def repr - subject_repr = - if @subject.respond_to?(:id) - "#{@subject.class.name}/#{@subject.id}" - else - @subject.inspect - end - - user_repr = - if @user - @user.to_reference - else - "<anonymous>" - end - - "(#{user_repr} : #{subject_repr})" - end - - def inspect - "#<#{self.class.name} #{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. - 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 - - # Helpers for caching. Used by ManifestCondition in performing condition - # computation. - # - # NOTE we can't use ||= here because the value might be the - # boolean `false` - def cache(key) - return @cache[key] if cached?(key) - - @cache[key] = yield - end - - def cached?(key) - !@cache[key].nil? - end - - # returns a ManifestCondition capable of computing itself. The computation - # will use our own @cache. - 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 - - # 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. - def banned? - global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) } - !Runner.new(global_steps).pass? - end - - # A list of other policies that we've delegated to (see `Base.delegate`) - 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 - - def policy_for(other_subject) - DeclarativePolicy.policy_for(@user, other_subject, cache: @cache) - end - - protected - - # constructs steps that come from this policy and not from any delegations - def own_steps(ability) - rules = self.class.configuration_for(ability) - rules.map { |(action, rule)| Step.new(self, rule, action) } - end - end -end diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb deleted file mode 100644 index 13006e56454..00000000000 --- a/lib/declarative_policy/cache.rb +++ /dev/null @@ -1,39 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/condition.rb b/lib/declarative_policy/condition.rb deleted file mode 100644 index b77f40b1093..00000000000 --- a/lib/declarative_policy/condition.rb +++ /dev/null @@ -1,105 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/delegate_dsl.rb b/lib/declarative_policy/delegate_dsl.rb deleted file mode 100644 index 67e3429b696..00000000000 --- a/lib/declarative_policy/delegate_dsl.rb +++ /dev/null @@ -1,18 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/policy_dsl.rb b/lib/declarative_policy/policy_dsl.rb deleted file mode 100644 index 69a2bbcc79e..00000000000 --- a/lib/declarative_policy/policy_dsl.rb +++ /dev/null @@ -1,46 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/preferred_scope.rb b/lib/declarative_policy/preferred_scope.rb deleted file mode 100644 index 9e512086593..00000000000 --- a/lib/declarative_policy/preferred_scope.rb +++ /dev/null @@ -1,31 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb deleted file mode 100644 index 964d35cde9e..00000000000 --- a/lib/declarative_policy/rule.rb +++ /dev/null @@ -1,312 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/rule_dsl.rb b/lib/declarative_policy/rule_dsl.rb deleted file mode 100644 index 85da7f261fa..00000000000 --- a/lib/declarative_policy/rule_dsl.rb +++ /dev/null @@ -1,47 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb deleted file mode 100644 index 59588b4d84e..00000000000 --- a/lib/declarative_policy/runner.rb +++ /dev/null @@ -1,196 +0,0 @@ -# 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 diff --git a/lib/declarative_policy/step.rb b/lib/declarative_policy/step.rb deleted file mode 100644 index c289c17cc19..00000000000 --- a/lib/declarative_policy/step.rb +++ /dev/null @@ -1,88 +0,0 @@ -# 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 diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 8385bbbb3de..d86eb83083b 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -3,6 +3,8 @@ module Gitlab module Diff class Highlight + PREFIX_REGEXP = /\A(.)/.freeze + attr_reader :diff_file, :diff_lines, :repository, :project delegate :old_path, :new_path, :old_sha, :new_sha, to: :diff_file, prefix: :diff @@ -97,12 +99,12 @@ module Gitlab rich_line = syntax_highlighter(diff_line).highlight( diff_line.text(prefix: false), context: { line_number: diff_line.line } - )&.html_safe + ) # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. if rich_line - line_prefix = diff_line.text =~ /\A(.)/ ? Regexp.last_match(1) : ' ' + line_prefix = diff_line.text =~ PREFIX_REGEXP ? Regexp.last_match(1) : ' ' rich_line.prepend(line_prefix).concat("\n") end end @@ -131,7 +133,7 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. if rich_line - line_prefix = diff_line.text =~ /\A(.)/ ? Regexp.last_match(1) : ' ' + line_prefix = diff_line.text =~ PREFIX_REGEXP ? Regexp.last_match(1) : ' ' "#{line_prefix}#{rich_line}".html_safe end end |