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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e206afe12d8b554e97e214bc27441b51ddf5252d3e4b1ee5800645bd27baa228
4
- data.tar.gz: cb1e6a7aa74f086fb70ebc7f30808db2b984660f6e375a49a5ee3467ebf0dbd2
3
+ metadata.gz: 2646404b27c4b7c45446b93b09ea1b9f488e26b66760599122950fae26982027
4
+ data.tar.gz: a61f1f277ddaf461a4629727fb0958f0bd6290eb1e83cd62658d23ceb65de08c
5
5
  SHA512:
6
- metadata.gz: 681910cfb585cef1e368db4e0f464b8866d35615cd1b2daaee1f02a45da613a52ec0a5063620ea36d2b1f47061486922227cc67cfa2d314b38f97a47c06fb7c8
7
- data.tar.gz: 16763d62485a9bf7be6d253fb68352e8da2edc2d7679b606b4b535fce67d2e301e39f236c649d4cb27930fb6b3b9a7daec5643ca9a51fb30e9e34bf14095e20a
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
- if aggregation.field.nil?
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
- return allowed.merge(to_check.to_h { |name| [name, true] }) unless permission_system?
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
- return if search.nil? || !collection.respond_to?(:searched_fields)
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
- return if searched.nil?
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 = args.dig(:params, :data, :attributes, :all_records_subset_query, :search) || args.dig(:params, :search)
254
+ search = subset_or_query(args, :search)
255
+
256
+ return nil if search.nil?
235
257
 
236
- raise BadRequestError, 'Collection is not searchable' if search && !collection.is_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.dig(:params, :data, :attributes, :all_records_subset_query,
243
- :searchExtended) || args.dig(:params, :searchExtended)
244
-
245
- return false if extended.nil?
268
+ extended = subset_or_query(args, :searchExtended)
246
269
 
247
- extended != '0'
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)
@@ -6,7 +6,7 @@ module ForestAdminAgent
6
6
  module Schema
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "agent-ruby"
9
- LIANA_VERSION = "1.41.0"
9
+ LIANA_VERSION = "1.43.0"
10
10
 
11
11
  def self.generate(datasource)
12
12
  datasource.collections
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.41.0"
2
+ VERSION = "1.43.0"
3
3
  end
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.41.0
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-01 00:00:00.000000000 Z
12
+ date: 2026-09-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport