forest_admin_agent 1.38.2 → 1.39.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/AUDIT_TRAIL.md +347 -0
- data/lib/forest_admin_agent/audit_trail/action_capture.rb +99 -0
- data/lib/forest_admin_agent/audit_trail/audit_record.rb +16 -0
- data/lib/forest_admin_agent/audit_trail/capture.rb +229 -0
- data/lib/forest_admin_agent/audit_trail/diff.rb +156 -0
- data/lib/forest_admin_agent/audit_trail/record_state.rb +43 -0
- data/lib/forest_admin_agent/audit_trail/recording.rb +57 -0
- data/lib/forest_admin_agent/audit_trail/snapshots.rb +85 -0
- data/lib/forest_admin_agent/audit_trail/sql/audit_connection_base.rb +53 -0
- data/lib/forest_admin_agent/audit_trail/sql/audit_log.rb +16 -0
- data/lib/forest_admin_agent/audit_trail/sql/field_filter.rb +58 -0
- data/lib/forest_admin_agent/audit_trail/sql/migrations.rb +53 -0
- data/lib/forest_admin_agent/audit_trail/sql/migrator.rb +117 -0
- data/lib/forest_admin_agent/audit_trail/sql/text_search.rb +78 -0
- data/lib/forest_admin_agent/audit_trail/store.rb +253 -0
- data/lib/forest_admin_agent/audit_trail.rb +68 -0
- data/lib/forest_admin_agent/builder/agent_factory.rb +20 -0
- data/lib/forest_admin_agent/http/correlation_id.rb +37 -0
- data/lib/forest_admin_agent/http/correlation_id_middleware.rb +29 -0
- data/lib/forest_admin_agent/http/router.rb +6 -0
- data/lib/forest_admin_agent/routes/action/actions.rb +76 -1
- data/lib/forest_admin_agent/routes/capabilities/collections.rb +11 -1
- data/lib/forest_admin_agent/routes/resources/audit_trail.rb +237 -0
- data/lib/forest_admin_agent/routes/resources/audit_trail_correlation.rb +101 -0
- data/lib/forest_admin_agent/routes/resources/audit_trail_route.rb +115 -0
- data/lib/forest_admin_agent/routes/resources/list.rb +1 -1
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +1 -1
- data/lib/forest_admin_agent/routes/resources/show.rb +1 -1
- data/lib/forest_admin_agent/serializer/forest_serializer_override.rb +14 -13
- data/lib/forest_admin_agent/utils/caller_parser.rb +4 -0
- data/lib/forest_admin_agent/utils/query_string_parser.rb +59 -10
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- data/lib/forest_admin_agent.rb +4 -0
- metadata +23 -2
|
@@ -54,23 +54,24 @@ module ForestAdminAgent
|
|
|
54
54
|
# Full linkage: a request for comments.author MUST automatically include comments
|
|
55
55
|
# in the response.
|
|
56
56
|
objects = is_collection ? object : [object]
|
|
57
|
+
|
|
58
|
+
relation = ForestAdminAgent::Facades::Container.datasource
|
|
59
|
+
.get_collection(options[:class_name].gsub('::', '__'))
|
|
60
|
+
.schema[:fields][attribute_name]
|
|
61
|
+
if relation.type == 'PolymorphicManyToOne'
|
|
62
|
+
relation_class_name = root_object[relation.foreign_key_type_field]
|
|
63
|
+
else
|
|
64
|
+
relation_class_name = ForestAdminAgent::Facades::Container.datasource.get_collection(relation.foreign_collection).name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
related_collection_options = options.merge(class_name: relation_class_name)
|
|
68
|
+
|
|
57
69
|
if child_inclusion_tree[:_include] == true
|
|
58
70
|
# Include the current level objects if the _include attribute exists.
|
|
59
71
|
# If it is not set, that indicates that this is an inner path and not a leaf and will
|
|
60
72
|
# be followed by the recursion below.
|
|
61
73
|
objects.each do |obj|
|
|
62
|
-
|
|
63
|
-
.get_collection(options[:class_name].gsub('::', '__'))
|
|
64
|
-
.schema[:fields][attribute_name]
|
|
65
|
-
if relation.type == 'PolymorphicManyToOne'
|
|
66
|
-
relation_class_name = root_object[relation.foreign_key_type_field]
|
|
67
|
-
else
|
|
68
|
-
relation_class_name = ForestAdminAgent::Facades::Container.datasource.get_collection(relation.foreign_collection).name
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
option_relation = options.clone
|
|
72
|
-
option_relation[:class_name] = relation_class_name
|
|
73
|
-
obj_serializer = JSONAPI::Serializer.find_serializer(obj, option_relation)
|
|
74
|
+
obj_serializer = JSONAPI::Serializer.find_serializer(obj, related_collection_options)
|
|
74
75
|
|
|
75
76
|
# Use keys of ['posts', '1'] for the results to enforce uniqueness.
|
|
76
77
|
# Spec: A compound document MUST NOT include more than one resource object for each
|
|
@@ -105,7 +106,7 @@ module ForestAdminAgent
|
|
|
105
106
|
|
|
106
107
|
# For each object we just loaded, find all deeper recursive relationships.
|
|
107
108
|
objects.each do |obj|
|
|
108
|
-
find_recursive_relationships(obj, child_inclusion_tree, results,
|
|
109
|
+
find_recursive_relationships(obj, child_inclusion_tree, results, related_collection_options)
|
|
109
110
|
end
|
|
110
111
|
end
|
|
111
112
|
nil
|
|
@@ -18,6 +18,10 @@ module ForestAdminAgent
|
|
|
18
18
|
@token_data = decode_token
|
|
19
19
|
@token_data[:timezone] = extract_timezone
|
|
20
20
|
@token_data[:request] = { ip: @args[:headers]['action_dispatch.remote_ip'].to_s }
|
|
21
|
+
# One id per request, generated by the agent, shared by every operation it triggers (used to
|
|
22
|
+
# correlate the audit trail) and echoed back to the client in the response header.
|
|
23
|
+
# See ForestAdminAgent::Http::CorrelationId.
|
|
24
|
+
@token_data[:request_id] = Http::CorrelationId.current
|
|
21
25
|
project, environment = extract_forest_context
|
|
22
26
|
@token_data[:project] = project
|
|
23
27
|
@token_data[:environment] = environment
|
|
@@ -9,6 +9,7 @@ module ForestAdminAgent
|
|
|
9
9
|
|
|
10
10
|
DEFAULT_ITEMS_PER_PAGE = '15'.freeze
|
|
11
11
|
DEFAULT_PAGE_TO_SKIP = '1'.freeze
|
|
12
|
+
POLYMORPHIC_TARGET_WILDCARD = '*'.freeze
|
|
12
13
|
|
|
13
14
|
def self.parse_condition_tree(collection, args)
|
|
14
15
|
filters = begin
|
|
@@ -88,7 +89,7 @@ module ForestAdminAgent
|
|
|
88
89
|
when 'Column'
|
|
89
90
|
field_name
|
|
90
91
|
when 'PolymorphicManyToOne'
|
|
91
|
-
"#{field_name}
|
|
92
|
+
"#{field_name}:#{POLYMORPHIC_TARGET_WILDCARD}"
|
|
92
93
|
else
|
|
93
94
|
relation_fields = args.dig(:params, :fields, field_name)
|
|
94
95
|
|
|
@@ -110,18 +111,63 @@ module ForestAdminAgent
|
|
|
110
111
|
root_field_names_with_types = root_field_names.dup
|
|
111
112
|
add_polymorphic_type_fields(collection, root_field_names_with_types)
|
|
112
113
|
|
|
113
|
-
projection_fields = requested_paths.map
|
|
114
|
-
field_name, nested_path = path.split(':', 2)
|
|
115
|
-
field = get_field(collection, field_name)
|
|
114
|
+
projection_fields = requested_paths.map { |path| expand_polymorphic_leaf(collection, path) }
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
projection_fields |
|
|
117
|
+
(root_field_names_with_types - root_field_names) |
|
|
118
|
+
nested_polymorphic_linkage_fields(collection, projection_fields)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.expand_polymorphic_leaf(collection, path)
|
|
122
|
+
segments = path.split(':')
|
|
123
|
+
leaf_index = segments.size - 1
|
|
124
|
+
|
|
125
|
+
each_field_along_path(collection, segments) do |field, index|
|
|
126
|
+
return "#{path}:#{POLYMORPHIC_TARGET_WILDCARD}" if polymorphic_many_to_one?(field) && index == leaf_index
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
path
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.nested_polymorphic_linkage_fields(collection, projection_fields)
|
|
133
|
+
projection_fields.flat_map do |path|
|
|
134
|
+
segments = path.split(':')
|
|
135
|
+
|
|
136
|
+
each_field_along_path(collection, segments).flat_map do |field, index|
|
|
137
|
+
next [] unless polymorphic_many_to_one?(field) && index.positive?
|
|
138
|
+
|
|
139
|
+
polymorphic_linkage_columns(field, segments[0...index])
|
|
121
140
|
end
|
|
122
141
|
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.polymorphic_linkage_columns(field, relation_path)
|
|
145
|
+
[field.foreign_key_type_field, field.foreign_key].map { |column| (relation_path + [column]).join(':') }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.each_field_along_path(collection, segments)
|
|
149
|
+
return to_enum(:each_field_along_path, collection, segments) unless block_given?
|
|
150
|
+
|
|
151
|
+
current_collection = collection
|
|
152
|
+
|
|
153
|
+
segments.each_with_index do |segment, index|
|
|
154
|
+
field = field_along_path(current_collection, segment, root: index.zero?)
|
|
155
|
+
break if field.nil?
|
|
156
|
+
|
|
157
|
+
yield field, index
|
|
158
|
+
|
|
159
|
+
break unless field.respond_to?(:foreign_collection)
|
|
160
|
+
|
|
161
|
+
current_collection = collection.datasource.get_collection(field.foreign_collection)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.field_along_path(collection, segment, root:)
|
|
166
|
+
root ? get_field(collection, segment) : collection.schema[:fields][segment]
|
|
167
|
+
end
|
|
123
168
|
|
|
124
|
-
|
|
169
|
+
def self.polymorphic_many_to_one?(field)
|
|
170
|
+
field.type == 'PolymorphicManyToOne'
|
|
125
171
|
end
|
|
126
172
|
|
|
127
173
|
def self.get_field(collection, field_name)
|
|
@@ -135,7 +181,10 @@ module ForestAdminAgent
|
|
|
135
181
|
'Please check if the field name is correct.'
|
|
136
182
|
end
|
|
137
183
|
private_class_method :add_polymorphic_type_fields, :build_projection_fields,
|
|
138
|
-
:build_header_projection_fields, :get_field
|
|
184
|
+
:build_header_projection_fields, :get_field,
|
|
185
|
+
:expand_polymorphic_leaf, :nested_polymorphic_linkage_fields,
|
|
186
|
+
:polymorphic_linkage_columns, :each_field_along_path,
|
|
187
|
+
:field_along_path, :polymorphic_many_to_one?
|
|
139
188
|
|
|
140
189
|
def self.parse_projection_with_pks(collection, args)
|
|
141
190
|
parse_projection_from_request(collection, args).with_pks(collection)
|
data/lib/forest_admin_agent.rb
CHANGED
|
@@ -4,7 +4,11 @@ require 'zeitwerk'
|
|
|
4
4
|
|
|
5
5
|
loader = Zeitwerk::Loader.for_gem
|
|
6
6
|
loader.inflector.inflect('oauth2' => 'OAuth2')
|
|
7
|
+
loader.inflector.inflect('sql' => 'Sql')
|
|
7
8
|
loader.inflector.inflect('sse_cache_invalidation' => 'SSECacheInvalidation')
|
|
9
|
+
# ActiveRecord is only needed by agents configuring an audit-trail database, and Rails eager loads
|
|
10
|
+
# every gem loader (Zeitwerk::Loader.eager_load_all), so these files must stay strictly autoloaded.
|
|
11
|
+
loader.do_not_eager_load("#{__dir__}/forest_admin_agent/audit_trail/sql")
|
|
8
12
|
loader.setup
|
|
9
13
|
|
|
10
14
|
module ForestAdminAgent
|
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.39.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-08-
|
|
12
|
+
date: 2026-08-19 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|
|
@@ -330,10 +330,26 @@ extensions: []
|
|
|
330
330
|
extra_rdoc_files: []
|
|
331
331
|
files:
|
|
332
332
|
- ".rspec"
|
|
333
|
+
- AUDIT_TRAIL.md
|
|
333
334
|
- Rakefile
|
|
334
335
|
- forest_admin
|
|
335
336
|
- forest_admin_agent.gemspec
|
|
336
337
|
- lib/forest_admin_agent.rb
|
|
338
|
+
- lib/forest_admin_agent/audit_trail.rb
|
|
339
|
+
- lib/forest_admin_agent/audit_trail/action_capture.rb
|
|
340
|
+
- lib/forest_admin_agent/audit_trail/audit_record.rb
|
|
341
|
+
- lib/forest_admin_agent/audit_trail/capture.rb
|
|
342
|
+
- lib/forest_admin_agent/audit_trail/diff.rb
|
|
343
|
+
- lib/forest_admin_agent/audit_trail/record_state.rb
|
|
344
|
+
- lib/forest_admin_agent/audit_trail/recording.rb
|
|
345
|
+
- lib/forest_admin_agent/audit_trail/snapshots.rb
|
|
346
|
+
- lib/forest_admin_agent/audit_trail/sql/audit_connection_base.rb
|
|
347
|
+
- lib/forest_admin_agent/audit_trail/sql/audit_log.rb
|
|
348
|
+
- lib/forest_admin_agent/audit_trail/sql/field_filter.rb
|
|
349
|
+
- lib/forest_admin_agent/audit_trail/sql/migrations.rb
|
|
350
|
+
- lib/forest_admin_agent/audit_trail/sql/migrator.rb
|
|
351
|
+
- lib/forest_admin_agent/audit_trail/sql/text_search.rb
|
|
352
|
+
- lib/forest_admin_agent/audit_trail/store.rb
|
|
337
353
|
- lib/forest_admin_agent/auth/auth_manager.rb
|
|
338
354
|
- lib/forest_admin_agent/auth/oauth2/forest_provider.rb
|
|
339
355
|
- lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb
|
|
@@ -344,6 +360,8 @@ files:
|
|
|
344
360
|
- lib/forest_admin_agent/facades/whitelist.rb
|
|
345
361
|
- lib/forest_admin_agent/http/Exceptions/business_error.rb
|
|
346
362
|
- lib/forest_admin_agent/http/Exceptions/http_exception.rb
|
|
363
|
+
- lib/forest_admin_agent/http/correlation_id.rb
|
|
364
|
+
- lib/forest_admin_agent/http/correlation_id_middleware.rb
|
|
347
365
|
- lib/forest_admin_agent/http/error_translator.rb
|
|
348
366
|
- lib/forest_admin_agent/http/forest_admin_api_requester.rb
|
|
349
367
|
- lib/forest_admin_agent/http/router.rb
|
|
@@ -357,6 +375,9 @@ files:
|
|
|
357
375
|
- lib/forest_admin_agent/routes/charts/charts.rb
|
|
358
376
|
- lib/forest_admin_agent/routes/query_handler.rb
|
|
359
377
|
- lib/forest_admin_agent/routes/request_context.rb
|
|
378
|
+
- lib/forest_admin_agent/routes/resources/audit_trail.rb
|
|
379
|
+
- lib/forest_admin_agent/routes/resources/audit_trail_correlation.rb
|
|
380
|
+
- lib/forest_admin_agent/routes/resources/audit_trail_route.rb
|
|
360
381
|
- lib/forest_admin_agent/routes/resources/count.rb
|
|
361
382
|
- lib/forest_admin_agent/routes/resources/csv.rb
|
|
362
383
|
- lib/forest_admin_agent/routes/resources/delete.rb
|