Class: ForestAdminAgent::Services::Permissions

Inherits:
Object
  • Object
show all
Includes:
Http::Exceptions, Utils, ForestAdminDatasourceToolkit::Components::Query::ConditionTree, ForestAdminDatasourceToolkit::Exceptions
Defined in:
lib/forest_admin_agent/services/permissions.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(caller) ⇒ Permissions

Returns a new instance of Permissions.



14
15
16
17
18
19
20
21
22
# File 'lib/forest_admin_agent/services/permissions.rb', line 14

def initialize(caller)
  @caller = caller
  @forest_api = ForestAdminAgent::Http::ForestAdminApiRequester.new
  @cache = FileCache.new(
    'permissions',
    Facades::Container.config_from_cache[:cache_dir].to_s,
    Facades::Container.config_from_cache[:permission_expiration]
  )
end

Instance Attribute Details

#cacheObject (readonly)

Returns the value of attribute cache.



12
13
14
# File 'lib/forest_admin_agent/services/permissions.rb', line 12

def cache
  @cache
end

#callerObject (readonly)

Returns the value of attribute caller.



12
13
14
# File 'lib/forest_admin_agent/services/permissions.rb', line 12

def caller
  @caller
end

#forest_apiObject (readonly)

Returns the value of attribute forest_api.



12
13
14
# File 'lib/forest_admin_agent/services/permissions.rb', line 12

def forest_api
  @forest_api
end

Class Method Details

.invalidate_cache(id_cache = nil) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/forest_admin_agent/services/permissions.rb', line 24

def self.invalidate_cache(id_cache = nil)
  cache = FileCache.new(
    'permissions',
    Facades::Container.config_from_cache[:cache_dir].to_s,
    Facades::Container.config_from_cache[:permission_expiration]
  )

  cache.clear if id_cache.nil?

  cache.delete(id_cache) unless cache.get(id_cache).nil?

  ForestAdminAgent::Facades::Container.logger.log('Info', "Invalidating #{id_cache} cache..")
end

Instance Method Details

#assert_can_read_query_fields(collection, condition_tree: nil, sort: nil, search: nil, search_extended: false) ⇒ Object

Refused rather than redacted: dropping a condition widens the result set and dropping a sort clause silently reorders it, while both leak the value they touch anyway — a starts_with filter answers one guess per request without returning a column of its own.

The route passes the components it will actually apply, already parsed. A component it drops is simply one it does not pass — a count carries no sort, a chart neither sort nor search — so nothing has to be declared alongside the query and then kept in step with it. What is authorised here is what the filter carries, not a second parse of the same parameters.



111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/forest_admin_agent/services/permissions.rb', line 111

def assert_can_read_query_fields(collection, condition_tree: nil, sort: nil, search: nil,
                                 search_extended: false)
  usages = []

  # `projection` collects the leaf fields without touching the tree. The route applies this very
  # instance next, so a traversal that rebuilt a branch — as `for_each_leaf` does — would leave
  # the guard deciding what runs.
  condition_tree&.projection&.each { |path| usages << usage('filter on', collection, path) }
  sort&.each { |clause| usages << usage('sort on', collection, clause[:field]) }
  collect_search_usages(collection, search, search_extended, usages)

  assert_can_read_usages(collection.name, usages)
end

#assert_can_read_usages(root_collection_name, usages) ⇒ Object

Raises:



125
126
127
128
129
130
131
132
133
134
# File 'lib/forest_admin_agent/services/permissions.rb', line 125

def assert_can_read_usages(root_collection_name, usages)
  allowed = read_permissions(root_collection_name, usages.flat_map { |usage| usage[:collections] })
  denied = usages.find { |usage| !readable_leaves?(usage[:collections], allowed) }

  return unless denied

  raise ForbiddenError,
        "You cannot #{denied[:action]} '#{denied[:path]}': you are not allowed to read " \
        "#{leaf_label(denied[:collections])}."
end

#can?(action, collection, allow_fetch: false) ⇒ Boolean

Returns:

  • (Boolean)

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/forest_admin_agent/services/permissions.rb', line 38

def can?(action, collection, allow_fetch: false)
  return true unless permission_system?

  user_data = get_user_data(caller.id)
  collections_data = get_collections_permissions_data(force_fetch: allow_fetch)

  is_allowed = permission_allowed?(collections_data, collection, action, user_data)

  unless is_allowed
    collections_data = get_collections_permissions_data(force_fetch: true)
    is_allowed = permission_allowed?(collections_data, collection, action, user_data)
  end

  raise ForbiddenError, "You don't have permission to #{action} this collection." unless is_allowed

  is_allowed
end

#can_chart?(parameters) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/forest_admin_agent/services/permissions.rb', line 136

def can_chart?(parameters)
  attributes = sanitize_chart_parameters(parameters.deep_symbolize_keys)
  hash_request = "#{attributes[:type]}:#{array_hash(attributes)}"
  is_allowed = get_chart_data(caller.rendering_id).include?(hash_request)

  is_allowed ||= get_chart_data(caller.rendering_id, force_fetch: true).include?(hash_request)

  unless is_allowed
    ForestAdminAgent::Facades::Container.logger.log(
      'Debug',
      "User #{caller.id} cannot retrieve chart on rendering #{caller.rendering_id}"
    )
    raise ForbiddenError, "You don't have permission to access this collection."
  end

  ForestAdminAgent::Facades::Container.logger.log(
    'Debug',
    "User #{caller.id} can retrieve chart on rendering #{caller.rendering_id}"
  )

  is_allowed
end

#can_execute_query_segment?(collection, query, connection_name) ⇒ Boolean

Returns:

  • (Boolean)


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/forest_admin_agent/services/permissions.rb', line 159

def can_execute_query_segment?(collection, query, connection_name)
  user_data = get_user_data(caller.id)
  if %w[admin developer editor].include?(user_data&.dig(:permissionLevel))
    ForestAdminAgent::Facades::Container.logger.log(
      'Debug',
      "User #{caller.id} can retrieve SQL segment on rendering #{caller.rendering_id}"
    )
    return true
  end

  collection_permissions = get_collection_rendering_permissions(collection, force_fetch: false)

  is_allowed = segment_permissions_valid?(collection_permissions, query, connection_name)

  unless is_allowed
    collection_permissions = get_collection_rendering_permissions(collection, force_fetch: true)
    is_allowed = segment_permissions_valid?(collection_permissions, query, connection_name)
  end

  unless is_allowed
    ForestAdminAgent::Facades::Container.logger.log(
      'Debug',
      "User #{caller.id} cannot retrieve query segment on rendering #{caller.rendering_id}"
    )

    raise ForbiddenError, "You don't have permission to use this query segment."
  end

  ForestAdminAgent::Facades::Container.logger.log(
    'Debug',
    "User #{caller.id} can retrieve query segment on rendering #{caller.rendering_id}"
  )

  is_allowed
end

#can_smart_action?(request, collection, filter, allow_fetch: true) ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/forest_admin_agent/services/permissions.rb', line 195

def can_smart_action?(request, collection, filter, allow_fetch: true)
  return true unless permission_system?

  user_data = get_user_data(caller.id)
  collections_data = get_collections_permissions_data(force_fetch: allow_fetch)
  action = find_action_from_endpoint(collection.name, request[:headers]['REQUEST_PATH'], request[:headers]['REQUEST_METHOD'])

  collection_actions = validate_smart_action_permissions(collections_data, collection, action)

  smart_action_approval = SmartActionChecker.new(
    request[:params],
    collection,
    # The schema scope lets the checker skip select-all resolution for global actions.
    collection_actions[action['name'].to_sym].merge(scope: collection.schema[:actions][action['name']]&.scope),
    caller,
    user_data[:roleId],
    filter
  )

  is_allowed = smart_action_approval.can_execute?
  ForestAdminAgent::Facades::Container.logger.log(
    'Debug',
    "User #{user_data[:roleId]} is #{"not" unless is_allowed} allowed to perform #{action["name"]}"
  )

  is_allowed
end

#get_scope(collection) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/forest_admin_agent/services/permissions.rb', line 223

def get_scope(collection)
  permissions = get_rendering_data(caller.rendering_id)
  scope = permissions[:scopes][collection.name.to_sym]

  return nil if scope.nil?

  team = get_team(caller.rendering_id)
  user = get_user_data(caller.id)

  if team.nil? || user.nil?
    raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
          "Unable to resolve the caller's team or user data while computing the permission " \
          "scope for '#{collection.name}'."
  end

  context_variables = ContextVariables.new(team, user)

  ContextVariablesInjector.inject_context_in_filter(scope, context_variables)
end

#get_segments(collection, force_fetch: false) ⇒ Object



243
244
245
246
247
# File 'lib/forest_admin_agent/services/permissions.rb', line 243

def get_segments(collection, force_fetch: false)
  permissions = get_rendering_data(caller.rendering_id, force_fetch: force_fetch)

  permissions[:segments][collection.name.to_sym]
end

#get_team(rendering_id) ⇒ Object



264
265
266
267
268
# File 'lib/forest_admin_agent/services/permissions.rb', line 264

def get_team(rendering_id)
  permissions = get_rendering_data(rendering_id)

  permissions[:team]
end

#get_user_data(user_id) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/forest_admin_agent/services/permissions.rb', line 249

def get_user_data(user_id)
  cache.get_or_set('forest.users') do
    response = fetch('/liana/v4/permissions/users')
    users = {}

    response.each do |user|
      users[user[:id].to_s] = user
    end

    ForestAdminAgent::Facades::Container.logger.log('Debug', 'Refreshing user permissions cache')

    users
  end[user_id.to_s]
end

#read_permissions(root_collection_name, collection_names) ⇒ Object

root_collection_name is pinned to readable and never looked up: browse already gates a listing, read a get, and the signed hash a chart.

Answered once per collection for the whole request: redact_projection and assert_can_read_query_fields both ask on a listing, and a chart asks three times.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/forest_admin_agent/services/permissions.rb', line 61

def read_permissions(root_collection_name, collection_names)
  to_check = collection_names.uniq.reject { |name| name == root_collection_name }
  allowed = { root_collection_name => true }

  return allowed if to_check.empty?

  # An absent permission system is not a denial: `can?` allows everything there, and answering
  # anything else would redact every relation on a deployment that granted nothing to check.
  # `skip_relation_read_permissions` is the operator asking for that same answer on purpose.
  if skip_relation_read_permissions? || !permission_system?
    return allowed.merge(to_check.to_h { |name| [name, true] })
  end

  @read_permissions ||= {}
  missing = to_check - @read_permissions.keys
  @read_permissions.merge!(fetch_read_permissions(missing)) unless missing.empty?

  allowed.merge(@read_permissions.slice(*to_check))
end

#redact_projection(collection, projection, named_by_caller:) ⇒ Object

An unnamed field is dropped rather than refused: the default expansion covers every column of every to-one relation, so refusing would turn an ordinary listing into a 403 for a caller that asked for nothing.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/forest_admin_agent/services/permissions.rb', line 84

def redact_projection(collection, projection, named_by_caller:)
  owners = projection.to_h do |path|
    [path, ForestAdminDatasourceToolkit::Utils::FieldPath.leaf_collection_names(collection, path)]
  end
  allowed = read_permissions(collection.name, owners.values.flatten)
  readable = ->(path) { readable_leaves?(owners[path], allowed) }

  if named_by_caller
    denied = projection.reject { |path| readable.call(path) }

    unless denied.empty?
      fields = denied.map { |path| "'#{path}' from #{leaf_label(owners[path])}" }
      raise ForbiddenError, "You are not allowed to read #{fields.join(", ")}."
    end
  end

  ForestAdminDatasourceToolkit::Components::Query::Projection.new(projection.select { |path| readable.call(path) })
end