summaryrefslogtreecommitdiff
path: root/app/models/ability.rb
blob: 6a63a8d46ba9c61f3033fc2cb7c888c232bd42eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# frozen_string_literal: true

class Ability
  class << self
    # Given a list of users and a project this method returns the users that can
    # read the given project.
    def users_that_can_read_project(users, project)
      DeclarativePolicy.subject_scope do
        users.select { |u| allowed?(u, :read_project, project) }
      end
    end

    # Given a list of users and a group this method returns the users that can
    # read the given group.
    def users_that_can_read_group(users, group)
      DeclarativePolicy.subject_scope do
        users.select { |u| allowed?(u, :read_group, group) }
      end
    end

    # Given a list of users and a snippet this method returns the users that can
    # read the given snippet.
    def users_that_can_read_personal_snippet(users, snippet)
      DeclarativePolicy.subject_scope do
        users.select { |u| allowed?(u, :read_snippet, snippet) }
      end
    end

    # Returns an Array of Issues that can be read by the given user.
    #
    # issues - The issues to reduce down to those readable by the user.
    # user - The User for which to check the issues
    # filters - A hash of abilities and filters to apply if the user lacks this
    #           ability
    def issues_readable_by_user(issues, user = nil, filters: {})
      issues = apply_filters_if_needed(issues, user, filters)

      DeclarativePolicy.user_scope do
        issues.select { |issue| issue.visible_to_user?(user) }
      end
    end

    # Returns an Array of MergeRequests that can be read by the given user.
    #
    # merge_requests - MRs out of which to collect MRs readable by the user.
    # user - The User for which to check the merge_requests
    # filters - A hash of abilities and filters to apply if the user lacks this
    #           ability
    def merge_requests_readable_by_user(merge_requests, user = nil, filters: {})
      merge_requests = apply_filters_if_needed(merge_requests, user, filters)

      DeclarativePolicy.user_scope do
        merge_requests.select { |mr| allowed?(user, :read_merge_request, mr) }
      end
    end

    def allowed?(user, ability, subject = :global, opts = {})
      if subject.is_a?(Hash)
        opts = subject
        subject = :global
      end

      policy = policy_for(user, subject)

      case opts[:scope]
      when :user
        DeclarativePolicy.user_scope { policy.allowed?(ability) }
      when :subject
        DeclarativePolicy.subject_scope { policy.allowed?(ability) }
      else
        policy.allowed?(ability)
      end
    ensure
      # TODO: replace with runner invalidation:
      # See: https://gitlab.com/gitlab-org/declarative-policy/-/merge_requests/24
      # See: https://gitlab.com/gitlab-org/declarative-policy/-/merge_requests/25
      forget_runner_result(policy.runner(ability)) if policy && ability_forgetting?
    end

    def policy_for(user, subject = :global)
      DeclarativePolicy.policy_for(user, subject, cache: ::Gitlab::SafeRequestStore.storage)
    end

    # This method is something of a band-aid over the problem. The problem is
    # that some conditions may not be re-entrant, if facts change.
    # (`BasePolicy#admin?` is a known offender, due to the effects of
    # `admin_mode`)
    #
    # To deal with this we need to clear two elements of state: the offending
    # conditions (selected by 'pattern') and the cached ability checks (cached
    # on the `policy#runner(ability)`).
    #
    # Clearing the conditions (see `forget_all_but`) is fairly robust, provided
    # the pattern is not _under_-selective. Clearing the runners is harder,
    # since there is not good way to know which abilities any given condition
    # may affect. The approach taken here (see `forget_runner_result`) is to
    # discard all runner results generated during a `forgetting` block. This may
    # be _under_-selective if a runner prior to this block cached a state value
    # that might now be invalid.
    #
    # TODO: add some kind of reverse-dependency mapping in DeclarativePolicy
    # See: https://gitlab.com/gitlab-org/declarative-policy/-/issues/14
    def forgetting(pattern, &block)
      was_forgetting = ability_forgetting?
      ::Gitlab::SafeRequestStore[:ability_forgetting] = true
      keys_before = ::Gitlab::SafeRequestStore.storage.keys

      yield
    ensure
      ::Gitlab::SafeRequestStore[:ability_forgetting] = was_forgetting
      forget_all_but(keys_before, matching: pattern)
    end

    private

    def ability_forgetting?
      ::Gitlab::SafeRequestStore[:ability_forgetting]
    end

    def forget_all_but(keys_before, matching:)
      keys_after = ::Gitlab::SafeRequestStore.storage.keys

      added_keys = keys_after - keys_before
      added_keys.each do |key|
        if key.is_a?(String) && key.start_with?('/dp') && key =~ matching
          ::Gitlab::SafeRequestStore.delete(key)
        end
      end
    end

    def forget_runner_result(runner)
      # TODO: add support in DP for this
      # See: https://gitlab.com/gitlab-org/declarative-policy/-/issues/15
      runner.instance_variable_set(:@state, nil)
    end

    def apply_filters_if_needed(elements, user, filters)
      filters.each do |ability, filter|
        elements = filter.call(elements) unless allowed?(user, ability)
      end

      elements
    end
  end
end