forest_admin_agent 1.0.0.pre.beta.83 → 1.0.0.pre.beta.85
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/http/router.rb +1 -0
- data/lib/forest_admin_agent/routes/abstract_route.rb +2 -0
- data/lib/forest_admin_agent/routes/capabilities/collections.rb +9 -1
- data/lib/forest_admin_agent/routes/query_handler.rb +56 -0
- data/lib/forest_admin_agent/routes/resources/count.rb +12 -1
- data/lib/forest_admin_agent/routes/resources/csv.rb +2 -0
- data/lib/forest_admin_agent/routes/resources/list.rb +15 -12
- data/lib/forest_admin_agent/routes/resources/native_query.rb +117 -0
- data/lib/forest_admin_agent/routes/resources/update.rb +1 -2
- data/lib/forest_admin_agent/routes/security/scope_invalidation.rb +1 -1
- data/lib/forest_admin_agent/services/permissions.rb +63 -20
- data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +1 -1
- data/lib/forest_admin_agent/utils/context_variables_injector.rb +27 -2
- data/lib/forest_admin_agent/utils/query_validator.rb +73 -0
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea066cba15cc0f617f2d1c3a80268389401c379e137ea8eccf6311af1a47c74e
|
4
|
+
data.tar.gz: dd1be9288ee1bb1b2873d5d1b5eb2c76f6a2a48bfe836d4f13ed8b80b317f1d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c85e1a843858134e2e80b5c029f94abcffd853c0ff36994e0c3fd583c6f421b92ada593d2ba5e88189087b0edf0193ce8421c608e34459c14c945c5f523afd3e
|
7
|
+
data.tar.gz: b016730a166f3cc7007d7173504ea0136151d8019396cc592b5a90854bf2695f0684d3b6f0919cf53196de20b8b8a0e32178ff30cf0de5da6d0ba7e45dea9820
|
@@ -12,6 +12,7 @@ module ForestAdminAgent
|
|
12
12
|
Security::ScopeInvalidation.new.routes,
|
13
13
|
Charts::Charts.new.routes,
|
14
14
|
Capabilities::Collections.new.routes,
|
15
|
+
Resources::NativeQuery.new.routes,
|
15
16
|
Resources::Count.new.routes,
|
16
17
|
Resources::Delete.new.routes,
|
17
18
|
Resources::Csv.new.routes,
|
@@ -18,6 +18,13 @@ module ForestAdminAgent
|
|
18
18
|
@datasource = ForestAdminAgent::Facades::Container.datasource
|
19
19
|
collections = args[:params]['collectionNames'] || []
|
20
20
|
|
21
|
+
connections = []
|
22
|
+
ForestAdminAgent::Builder::AgentFactory.instance.customizer.datasources.map do |root_datasource|
|
23
|
+
connections = connections.union(
|
24
|
+
root_datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
21
28
|
result = collections.map do |collection_name|
|
22
29
|
collection = @datasource.get_collection(collection_name)
|
23
30
|
{
|
@@ -34,7 +41,8 @@ module ForestAdminAgent
|
|
34
41
|
|
35
42
|
{
|
36
43
|
content: {
|
37
|
-
collections: result
|
44
|
+
collections: result,
|
45
|
+
nativeQueryConnections: connections
|
38
46
|
},
|
39
47
|
status: 200
|
40
48
|
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Routes
|
3
|
+
module QueryHandler
|
4
|
+
include ForestAdminAgent::Utils
|
5
|
+
include ForestAdminAgent::Builder
|
6
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
7
|
+
include ForestAdminDatasourceToolkit::Validations
|
8
|
+
|
9
|
+
def inject_context_variables(connection_name, query, permissions, caller, context_variables)
|
10
|
+
user = permissions.get_user_data(caller.id)
|
11
|
+
team = permissions.get_team(caller.rendering_id)
|
12
|
+
context_variables = ContextVariables.new(team, user, context_variables)
|
13
|
+
|
14
|
+
ContextVariablesInjector.inject_context_in_native_query(connection_name, query, context_variables)
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute_query(query, connection_name, permissions, caller, context_variables)
|
18
|
+
root_datasource = AgentFactory.instance.customizer.get_root_datasource_by_connection(connection_name)
|
19
|
+
query = query.strip
|
20
|
+
query, context_variables = inject_context_variables(connection_name, query, permissions, caller,
|
21
|
+
context_variables)
|
22
|
+
|
23
|
+
root_datasource.execute_native_query(
|
24
|
+
connection_name,
|
25
|
+
query,
|
26
|
+
context_variables.values
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_query_segment(collection, args, permissions, caller)
|
31
|
+
return unless args[:params][:segmentQuery]
|
32
|
+
|
33
|
+
unless args[:params][:connectionName]
|
34
|
+
raise ForestAdminAgent::Http::Exceptions::UnprocessableError, 'Missing native query connection attribute'
|
35
|
+
end
|
36
|
+
|
37
|
+
QueryValidator.valid?(args[:params][:segmentQuery])
|
38
|
+
|
39
|
+
permissions.can_execute_query_segment?(collection, args[:params][:segmentQuery], args[:params][:connectionName])
|
40
|
+
|
41
|
+
ids = execute_query(
|
42
|
+
args[:params][:segmentQuery],
|
43
|
+
args[:params][:connectionName],
|
44
|
+
permissions,
|
45
|
+
caller,
|
46
|
+
args[:params][:contextVariables]
|
47
|
+
).map(&:values)
|
48
|
+
|
49
|
+
condition_tree_segment = ConditionTree::ConditionTreeFactory.match_ids(collection, ids)
|
50
|
+
ConditionTreeValidator.validate(condition_tree_segment, collection)
|
51
|
+
|
52
|
+
condition_tree_segment
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -6,6 +6,8 @@ module ForestAdminAgent
|
|
6
6
|
class Count < AbstractAuthenticatedRoute
|
7
7
|
include ForestAdminAgent::Builder
|
8
8
|
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
9
|
+
include ForestAdminAgent::Routes::QueryHandler
|
10
|
+
|
9
11
|
def setup_routes
|
10
12
|
add_route('forest_count', 'get', '/:collection_name/count', ->(args) { handle_request(args) })
|
11
13
|
|
@@ -18,7 +20,16 @@ module ForestAdminAgent
|
|
18
20
|
|
19
21
|
if @collection.is_countable?
|
20
22
|
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
21
|
-
condition_tree:
|
23
|
+
condition_tree: ConditionTreeFactory.intersect(
|
24
|
+
[
|
25
|
+
@permissions.get_scope(@collection),
|
26
|
+
parse_query_segment(@collection, args, @permissions, @caller),
|
27
|
+
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args)
|
28
|
+
]
|
29
|
+
),
|
30
|
+
search: QueryStringParser.parse_search(@collection, args),
|
31
|
+
search_extended: QueryStringParser.parse_search_extended(args),
|
32
|
+
segment: QueryStringParser.parse_segment(@collection, args)
|
22
33
|
)
|
23
34
|
aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
|
24
35
|
result = @collection.aggregate(@caller, filter, aggregation)
|
@@ -4,6 +4,7 @@ module ForestAdminAgent
|
|
4
4
|
class Csv < AbstractAuthenticatedRoute
|
5
5
|
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
6
6
|
include ForestAdminAgent::Utils
|
7
|
+
include ForestAdminAgent::Routes::QueryHandler
|
7
8
|
|
8
9
|
def setup_routes
|
9
10
|
add_route(
|
@@ -25,6 +26,7 @@ module ForestAdminAgent
|
|
25
26
|
condition_tree: ConditionTreeFactory.intersect(
|
26
27
|
[
|
27
28
|
@permissions.get_scope(@collection),
|
29
|
+
parse_query_segment(@collection, args, @permissions, @caller),
|
28
30
|
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
|
29
31
|
@collection, args
|
30
32
|
)
|
@@ -5,6 +5,8 @@ module ForestAdminAgent
|
|
5
5
|
module Resources
|
6
6
|
class List < AbstractAuthenticatedRoute
|
7
7
|
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
8
|
+
include ForestAdminAgent::Utils
|
9
|
+
include ForestAdminAgent::Routes::QueryHandler
|
8
10
|
|
9
11
|
def setup_routes
|
10
12
|
add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) })
|
@@ -17,20 +19,21 @@ module ForestAdminAgent
|
|
17
19
|
@permissions.can?(:browse, @collection)
|
18
20
|
|
19
21
|
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
20
|
-
condition_tree: ConditionTreeFactory.intersect(
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
condition_tree: ConditionTreeFactory.intersect(
|
23
|
+
[
|
24
|
+
@permissions.get_scope(@collection),
|
25
|
+
QueryStringParser.parse_condition_tree(@collection, args),
|
26
|
+
parse_query_segment(@collection, args, @permissions, @caller)
|
27
|
+
]
|
28
|
+
),
|
29
|
+
page: QueryStringParser.parse_pagination(args),
|
30
|
+
search: QueryStringParser.parse_search(@collection, args),
|
31
|
+
search_extended: QueryStringParser.parse_search_extended(args),
|
32
|
+
sort: QueryStringParser.parse_sort(@collection, args),
|
33
|
+
segment: QueryStringParser.parse_segment(@collection, args)
|
31
34
|
)
|
32
35
|
|
33
|
-
projection =
|
36
|
+
projection = QueryStringParser.parse_projection_with_pks(@collection, args)
|
34
37
|
records = @collection.list(@caller, filter, projection)
|
35
38
|
|
36
39
|
{
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
module ForestAdminAgent
|
5
|
+
module Routes
|
6
|
+
module Resources
|
7
|
+
class NativeQuery < AbstractAuthenticatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminAgent::Utils
|
10
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
11
|
+
include ForestAdminDatasourceToolkit::Components::Charts
|
12
|
+
include ForestAdminAgent::Routes::QueryHandler
|
13
|
+
|
14
|
+
def setup_routes
|
15
|
+
add_route(
|
16
|
+
'forest_native_query',
|
17
|
+
'post',
|
18
|
+
'/_internal/native_query',
|
19
|
+
lambda { |args|
|
20
|
+
handle_request(args)
|
21
|
+
}
|
22
|
+
)
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def handle_request(args = {})
|
28
|
+
build(args)
|
29
|
+
query = args[:params][:query].strip
|
30
|
+
|
31
|
+
QueryValidator.valid?(query)
|
32
|
+
unless args[:params][:connectionName]
|
33
|
+
raise ForestAdminAgent::Http::Exceptions::UnprocessableError, 'Missing native query connection attribute'
|
34
|
+
end
|
35
|
+
|
36
|
+
@permissions.can_chart?(args[:params])
|
37
|
+
|
38
|
+
query.gsub!('?', args[:params][:record_id].to_s) if args[:params][:record_id]
|
39
|
+
self.type = args[:params][:type]
|
40
|
+
result = execute_query(
|
41
|
+
query,
|
42
|
+
args[:params][:connectionName],
|
43
|
+
@permissions,
|
44
|
+
@caller,
|
45
|
+
args[:params][:contextVariables]
|
46
|
+
)
|
47
|
+
|
48
|
+
{ content: Serializer::ForestChartSerializer.serialize(send(:"make_#{@type}", result)) }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def type=(type)
|
54
|
+
chart_types = %w[Value Objective Pie Line Leaderboard]
|
55
|
+
unless chart_types.include?(type)
|
56
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Invalid Chart type #{type}"
|
57
|
+
end
|
58
|
+
|
59
|
+
@type = type.downcase
|
60
|
+
end
|
61
|
+
|
62
|
+
def raise_error(result, key_names)
|
63
|
+
raise ForestException,
|
64
|
+
"The result columns must be named #{key_names} instead of '#{result.keys.join("', '")}'"
|
65
|
+
end
|
66
|
+
|
67
|
+
def make_value(result)
|
68
|
+
return unless result.count
|
69
|
+
|
70
|
+
result = result.first
|
71
|
+
|
72
|
+
raise_error(result, "'value'") unless result.key?(:value)
|
73
|
+
|
74
|
+
ValueChart.new(result[:value] || 0, result[:previous] || nil).serialize
|
75
|
+
end
|
76
|
+
|
77
|
+
def make_objective(result)
|
78
|
+
return unless result.count
|
79
|
+
|
80
|
+
result = result.first
|
81
|
+
|
82
|
+
raise_error(result, "'value', 'objective'") unless result.key?(:value) || result.key?(:objective)
|
83
|
+
|
84
|
+
ObjectiveChart.new(result[:value] || 0, result[:objective]).serialize
|
85
|
+
end
|
86
|
+
|
87
|
+
def make_pie(result)
|
88
|
+
return unless result.count
|
89
|
+
|
90
|
+
raise_error(result[0], "'key', 'value'") if !result[0]&.key?(:value) || !result[0]&.key?(:key)
|
91
|
+
|
92
|
+
PieChart.new(result).serialize
|
93
|
+
end
|
94
|
+
|
95
|
+
def make_leaderboard(result)
|
96
|
+
return unless result.count
|
97
|
+
|
98
|
+
raise_error(result[0], "'key', 'value'") if !result[0]&.key?(:value) || !result[0]&.key?(:key)
|
99
|
+
|
100
|
+
LeaderboardChart.new(result).serialize
|
101
|
+
end
|
102
|
+
|
103
|
+
def make_line(result)
|
104
|
+
return unless result.count
|
105
|
+
|
106
|
+
result = result.map! do |result_line|
|
107
|
+
raise_error(result_line, "'key', 'value'") if !result_line.key?(:value) || !result_line.key?(:key)
|
108
|
+
|
109
|
+
{ label: result_line[:key], values: { value: result_line[:value] } }
|
110
|
+
end
|
111
|
+
|
112
|
+
LineChart.new(result).serialize
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -21,8 +21,7 @@ module ForestAdminAgent
|
|
21
21
|
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
22
22
|
condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
|
23
23
|
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
24
|
-
condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope])
|
25
|
-
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
|
24
|
+
condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope])
|
26
25
|
)
|
27
26
|
data = format_attributes(args)
|
28
27
|
@collection.update(@caller, filter, data)
|
@@ -18,7 +18,7 @@ module ForestAdminAgent
|
|
18
18
|
def handle_request(args)
|
19
19
|
# Check if user is logged
|
20
20
|
Utils::QueryStringParser.parse_caller(args)
|
21
|
-
Permissions.invalidate_cache('forest.
|
21
|
+
Permissions.invalidate_cache('forest.rendering')
|
22
22
|
|
23
23
|
{ content: nil, status: 204 }
|
24
24
|
end
|
@@ -80,6 +80,31 @@ module ForestAdminAgent
|
|
80
80
|
is_allowed
|
81
81
|
end
|
82
82
|
|
83
|
+
def can_execute_query_segment?(collection, query, connection_name)
|
84
|
+
hash_request = array_hash({ query: query, connectionName: connection_name })
|
85
|
+
is_allowed = get_segments(collection).include?(hash_request)
|
86
|
+
|
87
|
+
# Refetch
|
88
|
+
is_allowed ||= get_segments(collection, force_fetch: true).include?(hash_request)
|
89
|
+
|
90
|
+
# still not allowed - throw forbidden message
|
91
|
+
unless is_allowed
|
92
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
93
|
+
'Debug',
|
94
|
+
"User #{caller.id} cannot retrieve query segment on rendering #{caller.rendering_id}"
|
95
|
+
)
|
96
|
+
|
97
|
+
raise ForbiddenError, "You don't have permission to use this query segment."
|
98
|
+
end
|
99
|
+
|
100
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
101
|
+
'Debug',
|
102
|
+
"User #{caller.id} can retrieve query segment on rendering #{caller.rendering_id}"
|
103
|
+
)
|
104
|
+
|
105
|
+
is_allowed
|
106
|
+
end
|
107
|
+
|
83
108
|
def can_smart_action?(request, collection, filter, allow_fetch: true)
|
84
109
|
return true unless permission_system?
|
85
110
|
|
@@ -103,7 +128,7 @@ module ForestAdminAgent
|
|
103
128
|
end
|
104
129
|
|
105
130
|
def get_scope(collection)
|
106
|
-
permissions =
|
131
|
+
permissions = get_rendering_data(caller.rendering_id)
|
107
132
|
scope = permissions[:scopes][collection.name.to_sym]
|
108
133
|
|
109
134
|
return nil if scope.nil?
|
@@ -116,6 +141,12 @@ module ForestAdminAgent
|
|
116
141
|
ContextVariablesInjector.inject_context_in_filter(scope, context_variables)
|
117
142
|
end
|
118
143
|
|
144
|
+
def get_segments(collection, force_fetch: false)
|
145
|
+
permissions = get_rendering_data(caller.rendering_id, force_fetch: force_fetch)
|
146
|
+
|
147
|
+
permissions[:segments][collection.name.to_sym]
|
148
|
+
end
|
149
|
+
|
119
150
|
def get_user_data(user_id)
|
120
151
|
cache.get_or_set('forest.users') do
|
121
152
|
response = fetch('/liana/v4/permissions/users')
|
@@ -132,7 +163,7 @@ module ForestAdminAgent
|
|
132
163
|
end
|
133
164
|
|
134
165
|
def get_team(rendering_id)
|
135
|
-
permissions =
|
166
|
+
permissions = get_rendering_data(rendering_id)
|
136
167
|
|
137
168
|
permissions[:team]
|
138
169
|
end
|
@@ -157,35 +188,23 @@ module ForestAdminAgent
|
|
157
188
|
end
|
158
189
|
|
159
190
|
def get_chart_data(rendering_id, force_fetch: false)
|
160
|
-
|
161
|
-
|
162
|
-
cache.get_or_set('forest.stats') do
|
163
|
-
response = fetch("/liana/v4/permissions/renderings/#{rendering_id}")
|
164
|
-
stat_hash = []
|
165
|
-
response[:stats].each do |stat|
|
166
|
-
stat = stat.select { |_, value| !value.nil? && value != '' }
|
167
|
-
stat_hash << "#{stat[:type]}:#{array_hash(stat)}"
|
168
|
-
end
|
169
|
-
|
170
|
-
ForestAdminAgent::Facades::Container.logger.log(
|
171
|
-
'Debug',
|
172
|
-
"Loading rendering permissions for rendering #{rendering_id}"
|
173
|
-
)
|
191
|
+
rendering_data = get_rendering_data(rendering_id, force_fetch: force_fetch)
|
174
192
|
|
175
|
-
|
176
|
-
end
|
193
|
+
rendering_data[:charts]
|
177
194
|
end
|
178
195
|
|
179
196
|
def sanitize_chart_parameters(parameters)
|
180
197
|
parameters.delete(:timezone)
|
181
198
|
parameters.delete(:collection)
|
182
199
|
parameters.delete(:contextVariables)
|
200
|
+
parameters.delete(:record_id)
|
183
201
|
# rails
|
184
202
|
parameters.delete(:route_alias)
|
185
203
|
parameters.delete(:controller)
|
186
204
|
parameters.delete(:action)
|
187
205
|
parameters.delete(:collection_name)
|
188
206
|
parameters.delete(:forest)
|
207
|
+
parameters.delete(:format)
|
189
208
|
|
190
209
|
parameters.select { |_, value| !value.nil? && value != '' }
|
191
210
|
end
|
@@ -194,13 +213,17 @@ module ForestAdminAgent
|
|
194
213
|
Digest::SHA1.hexdigest(data.deep_sort.to_h.to_s)
|
195
214
|
end
|
196
215
|
|
197
|
-
def
|
198
|
-
|
216
|
+
def get_rendering_data(rendering_id, force_fetch: false)
|
217
|
+
self.class.invalidate_cache('forest.rendering') if force_fetch == true
|
218
|
+
|
219
|
+
cache.get_or_set('forest.rendering') do
|
199
220
|
data = {}
|
200
221
|
response = fetch("/liana/v4/permissions/renderings/#{rendering_id}")
|
201
222
|
|
202
223
|
data[:scopes] = decode_scope_permissions(response[:collections])
|
203
224
|
data[:team] = response[:team]
|
225
|
+
data[:segments] = decode_segment_permissions(response[:collections])
|
226
|
+
data[:charts] = decode_charts_permissions(response[:stats])
|
204
227
|
|
205
228
|
data
|
206
229
|
end
|
@@ -265,6 +288,26 @@ module ForestAdminAgent
|
|
265
288
|
scopes
|
266
289
|
end
|
267
290
|
|
291
|
+
def decode_charts_permissions(raw_permissions)
|
292
|
+
charts = []
|
293
|
+
|
294
|
+
raw_permissions.each do |chart|
|
295
|
+
chart = chart.select { |_, value| !value.nil? && value != '' }
|
296
|
+
charts << "#{chart[:type]}:#{array_hash(chart)}"
|
297
|
+
end
|
298
|
+
|
299
|
+
charts
|
300
|
+
end
|
301
|
+
|
302
|
+
def decode_segment_permissions(raw_permissions)
|
303
|
+
segments = {}
|
304
|
+
raw_permissions.each do |collection_name, value|
|
305
|
+
segments[collection_name] = value[:liveQuerySegments].map { |segment| array_hash(segment) }
|
306
|
+
end
|
307
|
+
|
308
|
+
segments
|
309
|
+
end
|
310
|
+
|
268
311
|
def fetch(url)
|
269
312
|
response = forest_api.get(url)
|
270
313
|
|
@@ -8,7 +8,7 @@ module ForestAdminAgent
|
|
8
8
|
MESSAGE_CACHE_KEYS = {
|
9
9
|
'refresh-users': %w[forest.users],
|
10
10
|
'refresh-roles': %w[forest.collections],
|
11
|
-
'refresh-renderings': %w[forest.collections forest.
|
11
|
+
'refresh-renderings': %w[forest.collections forest.rendering]
|
12
12
|
# TODO: add one for ip whitelist when server implement it
|
13
13
|
}.freeze
|
14
14
|
|
@@ -2,6 +2,9 @@ module ForestAdminAgent
|
|
2
2
|
module Utils
|
3
3
|
class ContextVariablesInjector
|
4
4
|
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes
|
5
|
+
include ForestAdminAgent::Builder
|
6
|
+
|
7
|
+
REGEX = /{{([^}]+)}}/
|
5
8
|
|
6
9
|
def self.inject_context_in_value(value, context_variables)
|
7
10
|
inject_context_in_value_custom(value) do |context_variable_key|
|
@@ -9,14 +12,36 @@ module ForestAdminAgent
|
|
9
12
|
end
|
10
13
|
end
|
11
14
|
|
15
|
+
def self.inject_context_in_native_query(connection_name, query, context_variables)
|
16
|
+
return query unless query.is_a?(String)
|
17
|
+
|
18
|
+
query_with_context_variables_injected = query
|
19
|
+
encountered_variables = {}
|
20
|
+
datasource = AgentFactory.instance.customizer.get_root_datasource_by_connection(connection_name)
|
21
|
+
|
22
|
+
while (match = REGEX.match(query_with_context_variables_injected))
|
23
|
+
context_variable_key = match[1]
|
24
|
+
|
25
|
+
next if encountered_variables.value?(context_variable_key)
|
26
|
+
|
27
|
+
index = datasource.build_binding_symbol(connection_name, encountered_variables)
|
28
|
+
query_with_context_variables_injected.gsub!(
|
29
|
+
/{{#{context_variable_key}}}/,
|
30
|
+
index
|
31
|
+
)
|
32
|
+
encountered_variables[index] = context_variables.get_value(context_variable_key)
|
33
|
+
end
|
34
|
+
|
35
|
+
[query_with_context_variables_injected, encountered_variables]
|
36
|
+
end
|
37
|
+
|
12
38
|
def self.inject_context_in_value_custom(value)
|
13
39
|
return value unless value.is_a?(String)
|
14
40
|
|
15
41
|
value_with_context_variables_injected = value
|
16
|
-
regex = /{{([^}]+)}}/
|
17
42
|
encountered_variables = []
|
18
43
|
|
19
|
-
while (match =
|
44
|
+
while (match = REGEX.match(value_with_context_variables_injected))
|
20
45
|
context_variable_key = match[1]
|
21
46
|
|
22
47
|
unless encountered_variables.include?(context_variable_key)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Utils
|
3
|
+
module QueryValidator
|
4
|
+
FORBIDDEN_KEYWORDS = %w[DROP DELETE INSERT UPDATE ALTER].freeze
|
5
|
+
INJECTION_PATTERNS = [
|
6
|
+
/\bOR\s+1=1\b/i # OR 1=1
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
def self.valid?(query)
|
10
|
+
query = query.strip
|
11
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Query cannot be empty.' if query.empty?
|
12
|
+
|
13
|
+
sanitized_query = remove_content_inside_strings(query)
|
14
|
+
check_select_only(sanitized_query)
|
15
|
+
check_semicolon_placement(sanitized_query)
|
16
|
+
check_forbidden_keywords(sanitized_query)
|
17
|
+
check_unbalanced_parentheses(sanitized_query)
|
18
|
+
check_sql_injection_patterns(sanitized_query)
|
19
|
+
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def check_select_only(query)
|
29
|
+
return if query.strip.upcase.start_with?('SELECT')
|
30
|
+
|
31
|
+
raise ForestException, 'Only SELECT queries are allowed.'
|
32
|
+
end
|
33
|
+
|
34
|
+
def check_semicolon_placement(query)
|
35
|
+
semicolon_count = query.scan(';').size
|
36
|
+
|
37
|
+
raise ForestException, 'Only one query is allowed.' if semicolon_count > 1
|
38
|
+
return if semicolon_count != 1 || query.strip[-1] == ';'
|
39
|
+
|
40
|
+
raise ForestException, 'Semicolon must only appear as the last character in the query.'
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_forbidden_keywords(query)
|
44
|
+
FORBIDDEN_KEYWORDS.each do |keyword|
|
45
|
+
if /\b#{Regexp.escape(keyword)}\b/i.match?(query)
|
46
|
+
raise ForestException, "The query contains forbidden keyword: #{keyword}."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_unbalanced_parentheses(query)
|
52
|
+
open_count = query.count('(')
|
53
|
+
close_count = query.count(')')
|
54
|
+
|
55
|
+
return if open_count == close_count
|
56
|
+
|
57
|
+
raise ForestException, 'The query contains unbalanced parentheses.'
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_sql_injection_patterns(query)
|
61
|
+
INJECTION_PATTERNS.each do |pattern|
|
62
|
+
raise ForestException, 'The query contains a potential SQL injection pattern.' if pattern.match?(query)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove_content_inside_strings(query)
|
67
|
+
# remove content inside single and double quotes
|
68
|
+
query.gsub(/'(?:[^']|\\')*'/, '').gsub(/"(?:[^"]|\\")*"/, '')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
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.0.0.pre.beta.
|
4
|
+
version: 1.0.0.pre.beta.85
|
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: 2024-12-
|
12
|
+
date: 2024-12-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -249,10 +249,12 @@ files:
|
|
249
249
|
- lib/forest_admin_agent/routes/charts/api_chart_collection.rb
|
250
250
|
- lib/forest_admin_agent/routes/charts/api_chart_datasource.rb
|
251
251
|
- lib/forest_admin_agent/routes/charts/charts.rb
|
252
|
+
- lib/forest_admin_agent/routes/query_handler.rb
|
252
253
|
- lib/forest_admin_agent/routes/resources/count.rb
|
253
254
|
- lib/forest_admin_agent/routes/resources/csv.rb
|
254
255
|
- lib/forest_admin_agent/routes/resources/delete.rb
|
255
256
|
- lib/forest_admin_agent/routes/resources/list.rb
|
257
|
+
- lib/forest_admin_agent/routes/resources/native_query.rb
|
256
258
|
- lib/forest_admin_agent/routes/resources/related/associate_related.rb
|
257
259
|
- lib/forest_admin_agent/routes/resources/related/count_related.rb
|
258
260
|
- lib/forest_admin_agent/routes/resources/related/csv_related.rb
|
@@ -280,6 +282,7 @@ files:
|
|
280
282
|
- lib/forest_admin_agent/utils/error_messages.rb
|
281
283
|
- lib/forest_admin_agent/utils/id.rb
|
282
284
|
- lib/forest_admin_agent/utils/query_string_parser.rb
|
285
|
+
- lib/forest_admin_agent/utils/query_validator.rb
|
283
286
|
- lib/forest_admin_agent/utils/schema/action_fields.rb
|
284
287
|
- lib/forest_admin_agent/utils/schema/forest_value_converter.rb
|
285
288
|
- lib/forest_admin_agent/utils/schema/frontend_filterable.rb
|