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 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