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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
# 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
# A list of users that can read confidential notes in a project
def users_that_can_read_internal_notes(users, note_parent)
DeclarativePolicy.subject_scope do
users.select { |u| allowed?(u, :reporter_access, note_parent) }
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 feature_flags_readable_by_user(feature_flags, user = nil, filters: {})
feature_flags = apply_filters_if_needed(feature_flags, user, filters)
DeclarativePolicy.user_scope do
feature_flags.select { |flag| allowed?(user, :read_feature_flag, flag) }
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
|