forest_admin_agent 1.0.0.pre.beta.83 → 1.0.0.pre.beta.85
Sign up to get free protection for your applications and to get access to all the features.
- 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
|