forest_admin_agent 1.41.0 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/forest_admin_agent/builder/agent_factory.rb +14 -0
- data/lib/forest_admin_agent/routes/capabilities/collections.rb +6 -1
- data/lib/forest_admin_agent/routes/charts/charts.rb +7 -2
- data/lib/forest_admin_agent/services/permissions.rb +37 -4
- data/lib/forest_admin_agent/utils/query_string_parser.rb +31 -8
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2646404b27c4b7c45446b93b09ea1b9f488e26b66760599122950fae26982027
|
|
4
|
+
data.tar.gz: a61f1f277ddaf461a4629727fb0958f0bd6290eb1e83cd62658d23ceb65de08c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b357902be6f9a8cfdd0ce5b74695911dd6d31cb7613c3e4bc93658a17691a6c8c7d02744271b15ecebf576e72bdb04c25836eef5ef45d9c234da4df97316d5c
|
|
7
|
+
data.tar.gz: d129a77b8b1b9fd9e2b96c1bcdb87b45f295062b3cdcab28be6b3975629ad5c3597fcdf4626bae67c2563d8975469129e01512460245900700131e2f05c6fbc0
|
|
@@ -27,6 +27,7 @@ module ForestAdminAgent
|
|
|
27
27
|
build_container
|
|
28
28
|
build_cache
|
|
29
29
|
build_logger
|
|
30
|
+
warn_relation_read_permissions_skipped
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def add_datasource(datasource, options = {})
|
|
@@ -341,6 +342,19 @@ module ForestAdminAgent
|
|
|
341
342
|
Facades::Container.cache(:skip_schema_update) == true
|
|
342
343
|
end
|
|
343
344
|
|
|
345
|
+
# An auditor reading the boot log should see the weakened posture without reading the config.
|
|
346
|
+
# Read from the options `setup` was handed: `Facades::Container` resolves against
|
|
347
|
+
# `AgentFactory.instance`, which a host subclassing this factory has not populated.
|
|
348
|
+
def warn_relation_read_permissions_skipped
|
|
349
|
+
return unless @options.to_h[:skip_relation_read_permissions] == true
|
|
350
|
+
|
|
351
|
+
@logger.log(
|
|
352
|
+
'Warn',
|
|
353
|
+
'[ForestAdmin] skip_relation_read_permissions is true: columns of collections the caller ' \
|
|
354
|
+
'has no read permission on are served when a relation path reaches them'
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
|
|
344
358
|
def log_schema_skip
|
|
345
359
|
@logger.log('Warn', '[ForestAdmin] Schema update skipped (skip_schema_update flag is true)')
|
|
346
360
|
environment = Facades::Container.cache(:is_production) ? 'production' : 'development'
|
|
@@ -68,7 +68,8 @@ module ForestAdminAgent
|
|
|
68
68
|
canUseProjectionViaHeader: true,
|
|
69
69
|
canUseProjectionViaHeaderOnList: true,
|
|
70
70
|
canUseMultipleFieldsProjectionOnRelation: true,
|
|
71
|
-
canUseAuditTrail: audit_trail_enabled
|
|
71
|
+
canUseAuditTrail: audit_trail_enabled?,
|
|
72
|
+
checksRelationReadPermissions: !skip_relation_read_permissions?
|
|
72
73
|
}
|
|
73
74
|
},
|
|
74
75
|
status: 200
|
|
@@ -83,6 +84,10 @@ module ForestAdminAgent
|
|
|
83
84
|
def audit_trail_enabled?
|
|
84
85
|
!::ForestAdminAgent::AuditTrail.store.nil?
|
|
85
86
|
end
|
|
87
|
+
|
|
88
|
+
def skip_relation_read_permissions?
|
|
89
|
+
ForestAdminAgent::Facades::Container.config_from_cache[:skip_relation_read_permissions] == true
|
|
90
|
+
end
|
|
86
91
|
end
|
|
87
92
|
end
|
|
88
93
|
end
|
|
@@ -55,6 +55,10 @@ module ForestAdminAgent
|
|
|
55
55
|
field.nil? || field.to_s.empty? ? nil : field
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
def skip_relation_read_permissions?
|
|
59
|
+
ForestAdminAgent::Facades::Container.config_from_cache[:skip_relation_read_permissions] == true
|
|
60
|
+
end
|
|
61
|
+
|
|
58
62
|
def validate_and_get_type(type)
|
|
59
63
|
chart_types = %w[Value Objective Pie Line Leaderboard]
|
|
60
64
|
unless chart_types.include?(type)
|
|
@@ -202,8 +206,9 @@ module ForestAdminAgent
|
|
|
202
206
|
)
|
|
203
207
|
|
|
204
208
|
# A count exposes the cardinality of the relation, which `/relationships/<name>/count`
|
|
205
|
-
# puts behind `browse`. No path names it, so nothing above sees it.
|
|
206
|
-
|
|
209
|
+
# puts behind `browse`. No path names it, so nothing above sees it. It arrived with the
|
|
210
|
+
# related-read checks, so it goes away with them.
|
|
211
|
+
if aggregation.field.nil? && !skip_relation_read_permissions?
|
|
207
212
|
context.permissions.can?(:browse, context.datasource.get_collection(field.foreign_collection))
|
|
208
213
|
end
|
|
209
214
|
|
|
@@ -66,7 +66,10 @@ module ForestAdminAgent
|
|
|
66
66
|
|
|
67
67
|
# An absent permission system is not a denial: `can?` allows everything there, and answering
|
|
68
68
|
# anything else would redact every relation on a deployment that granted nothing to check.
|
|
69
|
-
|
|
69
|
+
# `skip_relation_read_permissions` is the operator asking for that same answer on purpose.
|
|
70
|
+
if skip_relation_read_permissions? || !permission_system?
|
|
71
|
+
return allowed.merge(to_check.to_h { |name| [name, true] })
|
|
72
|
+
end
|
|
70
73
|
|
|
71
74
|
@read_permissions ||= {}
|
|
72
75
|
missing = to_check - @read_permissions.keys
|
|
@@ -273,6 +276,10 @@ module ForestAdminAgent
|
|
|
273
276
|
names.any? && names.all? { |name| allowed[name] }
|
|
274
277
|
end
|
|
275
278
|
|
|
279
|
+
def skip_relation_read_permissions?
|
|
280
|
+
Facades::Container.config_from_cache[:skip_relation_read_permissions] == true
|
|
281
|
+
end
|
|
282
|
+
|
|
276
283
|
def leaf_label(names)
|
|
277
284
|
names.empty? ? 'an unresolved polymorphic relation' : "the '#{names.join("' or '")}' collection"
|
|
278
285
|
end
|
|
@@ -330,11 +337,16 @@ module ForestAdminAgent
|
|
|
330
337
|
end
|
|
331
338
|
|
|
332
339
|
def collect_search_usages(collection, search, search_extended, usages)
|
|
333
|
-
|
|
340
|
+
# The stack discards a blank search instead of running it, so there is nothing to authorize.
|
|
341
|
+
return if search.nil? || search.strip.empty?
|
|
334
342
|
|
|
335
|
-
searched = collection.searched_fields(search, search_extended)
|
|
343
|
+
searched = collection.searched_fields(search, search_extended) if collection.respond_to?(:searched_fields)
|
|
336
344
|
|
|
337
|
-
|
|
345
|
+
if searched.nil?
|
|
346
|
+
assert_extended_search_checkable(collection, search_extended)
|
|
347
|
+
|
|
348
|
+
return
|
|
349
|
+
end
|
|
338
350
|
|
|
339
351
|
published = collection.datasource.collections
|
|
340
352
|
|
|
@@ -344,6 +356,27 @@ module ForestAdminAgent
|
|
|
344
356
|
end
|
|
345
357
|
end
|
|
346
358
|
|
|
359
|
+
# Only a callable `replace_search` is a footprint this stack could have described and did not; a
|
|
360
|
+
# native child search, or no search decorator at all, builds no condition here. A plain search is
|
|
361
|
+
# served in either case — the extended flag is the caller's own, so the exemption stops there.
|
|
362
|
+
def assert_extended_search_checkable(collection, search_extended)
|
|
363
|
+
return unless search_extended
|
|
364
|
+
return unless describes_own_search?(collection)
|
|
365
|
+
return unless permission_system?
|
|
366
|
+
# Refused ahead of `read_permissions`, so the skip has to be read here as well.
|
|
367
|
+
return if skip_relation_read_permissions?
|
|
368
|
+
|
|
369
|
+
raise ForbiddenError,
|
|
370
|
+
"You cannot run an extended search on the '#{collection.name}' collection: the fields " \
|
|
371
|
+
'it reaches cannot be determined, so they cannot be checked against your permissions.'
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Reaches the search decorator only through `CollectionDecorator#search_handler?`: drop that
|
|
375
|
+
# delegation and this answers false for every collection.
|
|
376
|
+
def describes_own_search?(collection)
|
|
377
|
+
collection.respond_to?(:search_handler?) && collection.search_handler?
|
|
378
|
+
end
|
|
379
|
+
|
|
347
380
|
# `searched_fields` answers below the publication layer — deliberately, so a field hidden by
|
|
348
381
|
# renaming above it is still checked — so it can name a collection `remove_collection` took out
|
|
349
382
|
# of the API. An extended search does reach through to it: the condition is built below
|
|
@@ -10,6 +10,7 @@ module ForestAdminAgent
|
|
|
10
10
|
DEFAULT_ITEMS_PER_PAGE = '15'.freeze
|
|
11
11
|
DEFAULT_PAGE_TO_SKIP = '1'.freeze
|
|
12
12
|
POLYMORPHIC_TARGET_WILDCARD = '*'.freeze
|
|
13
|
+
FALSY_SEARCH_EXTENDED = [nil, false, 0, '0', 'false', ''].freeze
|
|
13
14
|
|
|
14
15
|
def self.parse_condition_tree(collection, args)
|
|
15
16
|
filters = begin
|
|
@@ -230,21 +231,43 @@ module ForestAdminAgent
|
|
|
230
231
|
Page.new(offset: 0, limit: limit&.to_i)
|
|
231
232
|
end
|
|
232
233
|
|
|
234
|
+
# Presence, not truth: with +||+ a +false+ in the select-all body would silently lose to the
|
|
235
|
+
# query string. An explicit +null+ or +''+ there is read as absent rather than as "no search",
|
|
236
|
+
# so neither can widen a result set by discarding the term the URL carried.
|
|
237
|
+
def self.subset_or_query(args, key)
|
|
238
|
+
subset = begin
|
|
239
|
+
args.dig(:params, :data, :attributes, :all_records_subset_query)
|
|
240
|
+
rescue StandardError
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
return subset[key] if subset.is_a?(Hash) && !subset[key].nil? && subset[key] != ''
|
|
245
|
+
|
|
246
|
+
begin
|
|
247
|
+
args.dig(:params, key)
|
|
248
|
+
rescue StandardError
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
233
253
|
def self.parse_search(collection, args)
|
|
234
|
-
search =
|
|
254
|
+
search = subset_or_query(args, :search)
|
|
255
|
+
|
|
256
|
+
return nil if search.nil?
|
|
235
257
|
|
|
236
|
-
raise BadRequestError, 'Collection is not searchable'
|
|
258
|
+
raise BadRequestError, 'Collection is not searchable' unless collection.is_searchable?
|
|
237
259
|
|
|
238
|
-
search
|
|
260
|
+
unless search.is_a?(String) || search.is_a?(Numeric)
|
|
261
|
+
raise BadRequestError, 'Search must be a string or a number'
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
search.to_s
|
|
239
265
|
end
|
|
240
266
|
|
|
241
267
|
def self.parse_search_extended(args)
|
|
242
|
-
extended = args
|
|
243
|
-
:searchExtended) || args.dig(:params, :searchExtended)
|
|
244
|
-
|
|
245
|
-
return false if extended.nil?
|
|
268
|
+
extended = subset_or_query(args, :searchExtended)
|
|
246
269
|
|
|
247
|
-
extended
|
|
270
|
+
!FALSY_SEARCH_EXTENDED.include?(extended.is_a?(String) ? extended.downcase : extended)
|
|
248
271
|
end
|
|
249
272
|
|
|
250
273
|
def self.parse_sort(collection, args)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_admin_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.43.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthieu
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-09-
|
|
12
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|