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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc2616742091891ab31320bddf9c831fe4c02d5a87cf932a0e871ef2d38763c6
4
- data.tar.gz: b1339e1f05022f5b802acf7316e7f2e2aa3ec813e0266257056e1961170e31d2
3
+ metadata.gz: ea066cba15cc0f617f2d1c3a80268389401c379e137ea8eccf6311af1a47c74e
4
+ data.tar.gz: dd1be9288ee1bb1b2873d5d1b5eb2c76f6a2a48bfe836d4f13ed8b80b317f1d5
5
5
  SHA512:
6
- metadata.gz: 75f872e89c52353aebd3f2bc81aab8004db746fb5d1852cb63c2b5e7f2125a04e105c87f4b1e595a772d0928ee156c94bca01fad7dcb1764074366cfcc7dc96f
7
- data.tar.gz: 446338498854b9aaec0e1b00b68dca90230e371b202a55d14b4509c43e9e98875bc61bb5ac5cad2df405162ef1762d01cd7241874fc4f61e7422e9b91f99b97c
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,
@@ -8,6 +8,8 @@ module ForestAdminAgent
8
8
 
9
9
  def build(args)
10
10
  @datasource = ForestAdminAgent::Facades::Container.datasource
11
+ return unless args[:params]['collection_name']
12
+
11
13
  @collection = @datasource.get_collection(args[:params]['collection_name'])
12
14
  end
13
15
 
@@ -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: @permissions.get_scope(@collection)
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
- @permissions.get_scope(@collection),
22
- ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
23
- @collection, args
24
- )
25
- ]),
26
- page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
27
- search: ForestAdminAgent::Utils::QueryStringParser.parse_search(@collection, args),
28
- search_extended: ForestAdminAgent::Utils::QueryStringParser.parse_search_extended(args),
29
- sort: ForestAdminAgent::Utils::QueryStringParser.parse_sort(@collection, args),
30
- segment: ForestAdminAgent::Utils::QueryStringParser.parse_segment(@collection, args)
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 = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args)
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.scopes')
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 = get_scope_and_team_data(caller.rendering_id)
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 = get_scope_and_team_data(rendering_id)
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
- self.class.invalidate_cache('forest.stats') if force_fetch == true
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
- stat_hash
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 get_scope_and_team_data(rendering_id)
198
- cache.get_or_set('forest.scopes') do
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.stats forest.scopes]
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 = regex.match(value_with_context_variables_injected))
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
@@ -7,7 +7,7 @@ module ForestAdminAgent
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "agent-ruby"
9
9
 
10
- LIANA_VERSION = "1.0.0-beta.83"
10
+ LIANA_VERSION = "1.0.0-beta.85"
11
11
 
12
12
  def self.get_serialized_schema(datasource)
13
13
  schema_path = Facades::Container.cache(:schema_path)
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.0.0-beta.83"
2
+ VERSION = "1.0.0-beta.85"
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.0.0.pre.beta.83
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-13 00:00:00.000000000 Z
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