forest_admin_agent 1.0.0.pre.beta.21 → 1.0.0.pre.beta.23

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/forest_admin_agent.gemspec +3 -1
  3. data/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +1 -1
  4. data/lib/forest_admin_agent/auth/oidc_client_manager.rb +3 -2
  5. data/lib/forest_admin_agent/builder/agent_factory.rb +7 -9
  6. data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +1 -1
  7. data/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +14 -0
  8. data/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +14 -0
  9. data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +1 -1
  10. data/lib/forest_admin_agent/http/Exceptions/require_approval.rb +15 -0
  11. data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +32 -4
  12. data/lib/forest_admin_agent/http/router.rb +1 -0
  13. data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +1 -0
  14. data/lib/forest_admin_agent/routes/charts/charts.rb +214 -0
  15. data/lib/forest_admin_agent/routes/resources/count.rb +7 -4
  16. data/lib/forest_admin_agent/routes/resources/delete.rb +5 -1
  17. data/lib/forest_admin_agent/routes/resources/list.rb +9 -3
  18. data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +10 -1
  19. data/lib/forest_admin_agent/routes/resources/related/count_related.rb +2 -1
  20. data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +2 -0
  21. data/lib/forest_admin_agent/routes/resources/related/list_related.rb +9 -1
  22. data/lib/forest_admin_agent/routes/resources/related/update_related.rb +8 -1
  23. data/lib/forest_admin_agent/routes/resources/show.rb +5 -3
  24. data/lib/forest_admin_agent/routes/resources/store.rb +11 -10
  25. data/lib/forest_admin_agent/routes/resources/update.rb +4 -3
  26. data/lib/forest_admin_agent/serializer/forest_chart_serializer.rb +19 -0
  27. data/lib/forest_admin_agent/services/permissions.rb +268 -0
  28. data/lib/forest_admin_agent/services/smart_action_checker.rb +95 -0
  29. data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +45 -0
  30. data/lib/forest_admin_agent/utils/context_variables.rb +39 -0
  31. data/lib/forest_admin_agent/utils/context_variables_injector.rb +53 -0
  32. data/lib/forest_admin_agent/utils/query_string_parser.rb +1 -0
  33. data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
  34. data/lib/forest_admin_agent/version.rb +1 -1
  35. data/lib/forest_admin_agent.rb +1 -0
  36. metadata +43 -5
@@ -15,16 +15,18 @@ module ForestAdminAgent
15
15
 
16
16
  def handle_request(args = {})
17
17
  build(args)
18
+ @permissions.can?(:read, @collection)
19
+ scope = @permissions.get_scope(@collection)
18
20
  id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
19
- caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
20
21
  condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
21
22
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
22
- condition_tree: condition_tree,
23
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope]),
23
24
  page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
24
25
  )
26
+
25
27
  projection = ProjectionFactory.all(@collection)
26
28
 
27
- records = @collection.list(caller, filter, projection)
29
+ records = @collection.list(@caller, filter, projection)
28
30
 
29
31
  raise Http::Exceptions::NotFoundError, 'Record does not exists' unless records.size.positive?
30
32
 
@@ -15,6 +15,7 @@ module ForestAdminAgent
15
15
 
16
16
  def handle_request(args = {})
17
17
  build(args)
18
+ @permissions.can?(:add, @collection)
18
19
  data = format_attributes(args)
19
20
  record = @collection.create(@caller, data)
20
21
  link_one_to_one_relations(args, record)
@@ -32,17 +33,17 @@ module ForestAdminAgent
32
33
  def link_one_to_one_relations(args, record)
33
34
  args[:params][:data][:relationships]&.map do |field, value|
34
35
  schema = @collection.fields[field]
35
- if schema.type == 'OneToOne'
36
- id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
37
- foreign_collection = @datasource.collection(schema.foreign_collection)
38
- # Load the value that will be used as origin_key
39
- origin_value = record[schema.origin_key_target]
36
+ next unless schema.type == 'OneToOne'
40
37
 
41
- # update new relation (may update zero or one records).
42
- condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
43
- filter = Filter.new(condition_tree: condition_tree)
44
- foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
45
- end
38
+ id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
39
+ foreign_collection = @datasource.collection(schema.foreign_collection)
40
+ # Load the value that will be used as origin_key
41
+ origin_value = record[schema.origin_key_target]
42
+
43
+ # update new relation (may update zero or one records).
44
+ condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
45
+ filter = Filter.new(condition_tree: condition_tree)
46
+ foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
46
47
  end
47
48
  end
48
49
  end
@@ -16,16 +16,17 @@ module ForestAdminAgent
16
16
 
17
17
  def handle_request(args = {})
18
18
  build(args)
19
+ @permissions.can?(:edit, @collection)
20
+ scope = @permissions.get_scope(@collection)
19
21
  id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
20
- caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
21
22
  condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
22
23
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
23
- condition_tree: condition_tree,
24
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope]),
24
25
  page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
25
26
  )
26
27
  data = format_attributes(args)
27
28
  @collection.update(@caller, filter, data)
28
- records = @collection.list(caller, filter, ProjectionFactory.all(@collection))
29
+ records = @collection.list(@caller, filter, ProjectionFactory.all(@collection))
29
30
 
30
31
  {
31
32
  name: args[:params]['collection_name'],
@@ -0,0 +1,19 @@
1
+ require 'securerandom'
2
+
3
+ module ForestAdminAgent
4
+ module Serializer
5
+ class ForestChartSerializer
6
+ def self.serialize(chart)
7
+ {
8
+ data: {
9
+ id: SecureRandom.uuid,
10
+ type: 'stats',
11
+ attributes: {
12
+ value: chart.serialize
13
+ }
14
+ }
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,268 @@
1
+ require 'filecache'
2
+ require 'deepsort'
3
+
4
+ module ForestAdminAgent
5
+ module Services
6
+ class Permissions
7
+ include ForestAdminAgent::Http::Exceptions
8
+ include ForestAdminAgent::Utils
9
+ include ForestAdminDatasourceToolkit::Exceptions
10
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
11
+
12
+ attr_reader :caller, :forest_api, :cache
13
+
14
+ def initialize(caller)
15
+ @caller = caller
16
+ @forest_api = ForestAdminAgent::Http::ForestAdminApiRequester.new
17
+ @cache = FileCache.new(
18
+ 'permissions',
19
+ Facades::Container.config_from_cache[:cache_dir].to_s,
20
+ Facades::Container.config_from_cache[:permission_expiration]
21
+ )
22
+ end
23
+
24
+ def self.invalidate_cache(id_cache = nil)
25
+ cache = FileCache.new(
26
+ 'permissions',
27
+ Facades::Container.config_from_cache[:cache_dir].to_s,
28
+ Facades::Container.config_from_cache[:permission_expiration]
29
+ )
30
+
31
+ cache.clear if id_cache.nil?
32
+
33
+ cache.delete(id_cache) unless cache.get(id_cache).nil?
34
+
35
+ # TODO: HANDLE LOGGER
36
+ # logger.debug("Invalidating #{id_cache} cache..")
37
+ end
38
+
39
+ def can?(action, collection, allow_fetch: false)
40
+ return true unless permission_system?
41
+
42
+ user_data = get_user_data(caller.id)
43
+ collections_data = get_collections_permissions_data(force_fetch: allow_fetch)
44
+
45
+ is_allowed = collections_data.key?(collection.name.to_sym) && collections_data[collection.name.to_sym][action].include?(user_data[:roleId])
46
+
47
+ # Refetch
48
+ unless is_allowed
49
+ collections_data = get_collections_permissions_data(force_fetch: true)
50
+ is_allowed = collections_data[collection.name.to_sym][action].include?(user_data[:roleId])
51
+ end
52
+
53
+ # still not allowed - throw forbidden message
54
+ raise ForbiddenError, "You don't have permission to #{action} this collection." unless is_allowed
55
+
56
+ is_allowed
57
+ end
58
+
59
+ def can_chart?(parameters)
60
+ attributes = sanitize_chart_parameters(parameters.deep_symbolize_keys)
61
+ hash_request = "#{attributes[:type]}:#{array_hash(attributes)}"
62
+ is_allowed = get_chart_data(caller.rendering_id).include?(hash_request)
63
+
64
+ # Refetch
65
+ is_allowed ||= get_chart_data(caller.rendering_id, force_fetch: true).include?(hash_request)
66
+
67
+ # still not allowed - throw forbidden message
68
+ unless is_allowed
69
+ # TODO: HANDLE LOGGER
70
+ # logger.debug("User #{caller.id} cannot retrieve chart on rendering #{caller.rendering_id}")
71
+ raise ForbiddenError, "You don't have permission to access this collection."
72
+ end
73
+
74
+ # TODO: HANDLE LOGGER
75
+ # logger.debug("User #{caller.id} can retrieve chart on rendering #{caller.rendering_id}")
76
+
77
+ is_allowed
78
+ end
79
+
80
+ def can_smart_action?(request, collection, filter, allow_fetch: true)
81
+ return true unless permission_system?
82
+
83
+ user_data = get_user_data(caller.id)
84
+ collections_data = get_collections_permissions_data(force_fetch: allow_fetch)
85
+ action = find_action_from_endpoint(collection.name, request[:headers]['REQUEST_PATH'], request[:headers]['REQUEST_METHOD'])
86
+ smart_action_approval = SmartActionChecker.new(
87
+ request[:params],
88
+ collection,
89
+ collections_data[collection.name.to_sym][:actions][action[:name]],
90
+ caller,
91
+ user_data[:roleId],
92
+ filter
93
+ )
94
+
95
+ smart_action_approval.can_execute?
96
+ # TODO: HANDLE LOGGER
97
+ # logger.debug("User #{user_data[:roleId]} is #{is_allowed ? '' : 'not'} allowed to perform #{action['name']}")
98
+ end
99
+
100
+ def get_scope(collection)
101
+ permissions = get_scope_and_team_data(caller.rendering_id)
102
+ scope = permissions[:scopes][collection.name.to_sym]
103
+
104
+ return nil if scope.nil?
105
+
106
+ context_variables = ContextVariables.new(team, user)
107
+
108
+ ContextVariablesInjector.inject_context_in_filter(scope, context_variables)
109
+ end
110
+
111
+ def get_user_data(user_id)
112
+ cache.get_or_set('forest.users') do
113
+ response = fetch('/liana/v4/permissions/users')
114
+ users = {}
115
+
116
+ response.each do |user|
117
+ users[user[:id].to_s] = user
118
+ end
119
+
120
+ # TODO: HANDLE LOGGER
121
+ # logger.debug('Refreshing user permissions cache')
122
+
123
+ users
124
+ end[user_id.to_s]
125
+ end
126
+
127
+ def get_team(rendering_id)
128
+ permissions = get_scope_and_team_data(rendering_id)
129
+
130
+ permissions[:team]
131
+ end
132
+
133
+ private
134
+
135
+ def get_collections_permissions_data(force_fetch: false)
136
+ self.class.invalidate_cache('forest.collections') if force_fetch == true
137
+
138
+ cache.get_or_set('forest.collections') do
139
+ response = fetch('/liana/v4/permissions/environment')
140
+ collections = {}
141
+
142
+ response[:collections].each do |name, collection|
143
+ collections[name] = decode_crud_permissions(collection).merge(decode_action_permissions(collection))
144
+ end
145
+
146
+ # TODO: HANDLE LOGGER
147
+ # logger.debug('Fetching environment permissions')
148
+
149
+ collections
150
+ end
151
+ end
152
+
153
+ def get_chart_data(rendering_id, force_fetch: false)
154
+ self.class.invalidate_cache('forest.stats') if force_fetch == true
155
+
156
+ cache.get_or_set('forest.stats') do
157
+ response = fetch("/liana/v4/permissions/renderings/#{rendering_id}")
158
+ stat_hash = []
159
+ response[:stats].each do |stat|
160
+ stat = stat.select { |_, value| !value.nil? && value != '' }
161
+ stat_hash << "#{stat[:type]}:#{array_hash(stat)}"
162
+ end
163
+
164
+ # TODO: HANDLE LOGGER
165
+ # logger.debug("Loading rendering permissions for rendering #{rendering_id}")
166
+
167
+ stat_hash
168
+ end
169
+ end
170
+
171
+ def sanitize_chart_parameters(parameters)
172
+ parameters.delete(:timezone)
173
+ parameters.delete(:collection)
174
+ parameters.delete(:contextVariables)
175
+ # rails
176
+ parameters.delete(:route_alias)
177
+ parameters.delete(:controller)
178
+ parameters.delete(:action)
179
+ parameters.delete(:collection_name)
180
+ parameters.delete(:forest)
181
+
182
+ parameters.select { |_, value| !value.nil? && value != '' }
183
+ end
184
+
185
+ def array_hash(data)
186
+ Digest::SHA1.hexdigest(data.deep_sort.to_h.to_s)
187
+ end
188
+
189
+ def get_scope_and_team_data(rendering_id)
190
+ cache.get_or_set('forest.scopes') do
191
+ data = {}
192
+ response = fetch("/liana/v4/permissions/renderings/#{rendering_id}")
193
+
194
+ data[:scopes] = decode_scope_permissions(response[:collections])
195
+ data[:team] = response[:team]
196
+
197
+ data
198
+ end
199
+ end
200
+
201
+ def permission_system?
202
+ cache.get_or_set('forest.has_permission') do
203
+ response = fetch('/liana/v4/permissions/environment')
204
+ { enable: response != true }
205
+ end[:enable]
206
+ end
207
+
208
+ def find_action_from_endpoint(collection_name, endpoint, http_method)
209
+ schema_file = JSON.parse(File.read(Facades::Container.config_from_cache[:schema_path]))
210
+ actions = schema_file['collections']&.select { |collection| collection['name'] == collection_name }&.first&.dig('actions')
211
+
212
+ return nil if actions.nil? || actions.empty?
213
+
214
+ action = actions.find { |a| a['endpoint'] == endpoint && a['http_method'].casecmp(http_method).zero? }
215
+
216
+ raise ForestException, "The collection #{collection_name} does not have this smart action" if action.nil?
217
+
218
+ action
219
+ end
220
+
221
+ def decode_crud_permissions(collection)
222
+ {
223
+ browse: collection[:collection][:browseEnabled][:roles],
224
+ read: collection[:collection][:readEnabled][:roles],
225
+ edit: collection[:collection][:editEnabled][:roles],
226
+ add: collection[:collection][:addEnabled][:roles],
227
+ delete: collection[:collection][:deleteEnabled][:roles],
228
+ export: collection[:collection][:exportEnabled][:roles]
229
+ }
230
+ end
231
+
232
+ def decode_action_permissions(collection)
233
+ actions = {}
234
+ actions[:actions] = {}
235
+ collection[:actions].each do |id, action|
236
+ actions[:actions][id] = {
237
+ triggerEnabled: action[:triggerEnabled][:roles],
238
+ triggerConditions: action[:triggerConditions],
239
+ approvalRequired: action[:approvalRequired][:roles],
240
+ approvalRequiredConditions: action[:approvalRequiredConditions],
241
+ userApprovalEnabled: action[:userApprovalEnabled][:roles],
242
+ userApprovalConditions: action[:userApprovalConditions],
243
+ selfApprovalEnabled: action[:selfApprovalEnabled][:roles]
244
+ }
245
+ end
246
+
247
+ actions
248
+ end
249
+
250
+ def decode_scope_permissions(raw_permissions)
251
+ scopes = {}
252
+ raw_permissions.each do |collection_name, value|
253
+ scopes[collection_name] = ConditionTreeFactory.from_plain_object(value[:scope]) unless value[:scope].nil?
254
+ end
255
+
256
+ scopes
257
+ end
258
+
259
+ def fetch(url)
260
+ response = forest_api.get(url)
261
+
262
+ JSON.parse(response.body, symbolize_names: true)
263
+ rescue StandardError => e
264
+ forest_api.handle_response_error(e)
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,95 @@
1
+ module ForestAdminAgent
2
+ module Services
3
+ class SmartActionChecker
4
+ include ForestAdminAgent::Http::Exceptions
5
+ include ForestAdminAgent::Utils
6
+ include ForestAdminDatasourceToolkit::Utils
7
+ include ForestAdminDatasourceToolkit::Components::Query
8
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
9
+
10
+ attr_reader :parameters, :collection, :smart_action, :caller, :role_id, :filter, :attributes
11
+
12
+ TRIGGER_FORBIDDEN_ERROR = 'CustomActionTriggerForbiddenError'.freeze
13
+
14
+ REQUIRE_APPROVAL_ERROR = 'CustomActionRequiresApprovalError'.freeze
15
+
16
+ INVALID_ACTION_CONDITION_ERROR = 'InvalidActionConditionError'.freeze
17
+
18
+ def initialize(parameters, collection, smart_action, caller, role_id, filter)
19
+ @parameters = parameters
20
+ @collection = collection
21
+ @smart_action = smart_action
22
+ @caller = caller
23
+ @role_id = role_id
24
+ @filter = filter
25
+ @attributes = parameters[:data][:attributes]
26
+ end
27
+
28
+ def can_execute?
29
+ if attributes[:signed_approval_request].nil?
30
+ can_trigger?
31
+ else
32
+ can_approve?
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def can_approve?
39
+ if smart_action[:userApprovalEnabled].include?(role_id) &&
40
+ (smart_action[:userApprovalConditions].empty? || match_conditions(:userApprovalConditions)) &&
41
+ (attributes[:requester_id] != caller.id || smart_action[:selfApprovalEnabled].include?(role_id))
42
+ return true
43
+ end
44
+
45
+ raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR)
46
+ end
47
+
48
+ def can_trigger?
49
+ if smart_action[:triggerEnabled].include?(role_id) && !smart_action[:approvalRequired].include?(role_id)
50
+ return true if smart_action[:triggerConditions].empty? || match_conditions(:triggerConditions)
51
+ elsif smart_action[:approvalRequired].include?(role_id) && smart_action[:triggerEnabled].include?(role_id)
52
+ if smart_action[:approvalRequiredConditions].empty? || match_conditions(:approvalRequiredConditions)
53
+ raise RequireApproval.new(
54
+ 'This action requires to be approved.',
55
+ REQUIRE_APPROVAL_ERROR,
56
+ smart_action[:userApprovalEnabled]
57
+ )
58
+ elsif smart_action[:triggerConditions].empty? || match_conditions(:triggerConditions)
59
+ return true
60
+ end
61
+ end
62
+
63
+ raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR)
64
+ end
65
+
66
+ def match_conditions(condition_name)
67
+ pk = Schema.primary_keys(collection)[0]
68
+ condition_filter = if attributes[:all_records]
69
+ Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', attributes[:all_records_ids_excluded])
70
+ else
71
+ Nodes::ConditionTreeLeaf.new(pk, 'IN', attributes[:ids])
72
+ end
73
+
74
+ condition = smart_action[condition_name][0]['filter']
75
+ conditional_filter = filter.override(
76
+ condition_tree: ConditionTreeFactory.intersect(
77
+ [
78
+ ConditionTreeParser.from_plain_object(collection, condition),
79
+ filter.condition_tree,
80
+ condition_filter
81
+ ]
82
+ )
83
+ )
84
+ rows = collection.aggregate(caller, conditional_filter, Aggregation.new(operation: 'Count'))
85
+
86
+ (rows[0]['value'] || 0) == attributes[:ids].count
87
+ rescue StandardError
88
+ raise ConflictError.new(
89
+ 'The conditions to trigger this action cannot be verified. Please contact an administrator.',
90
+ INVALID_ACTION_CONDITION_ERROR
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,45 @@
1
+ require 'ld-eventsource'
2
+
3
+ module ForestAdminAgent
4
+ module Services
5
+ class SSECacheInvalidation
6
+ include ForestAdminDatasourceToolkit::Exceptions
7
+
8
+ MESSAGE_CACHE_KEYS = {
9
+ 'refresh-users': %w[forest.users],
10
+ 'refresh-roles': %w[forest.collections],
11
+ 'refresh-renderings': %w[forest.collections forest.stats forest.scopes]
12
+ # TODO: add one for ip whitelist when server implement it
13
+ }.freeze
14
+
15
+ def self.run
16
+ uri = "#{Facades::Container.config_from_cache[:forest_server_url]}/liana/v4/subscribe-to-events"
17
+ headers = {
18
+ 'forest-secret-key' => Facades::Container.config_from_cache[:env_secret],
19
+ 'Accept' => 'text/event-stream'
20
+ }
21
+
22
+ begin
23
+ SSE::Client.new(uri, headers: headers) do |client|
24
+ client.on_event do |event|
25
+ next if event.type == :heartbeat
26
+
27
+ MESSAGE_CACHE_KEYS[event.type]&.each do |cache_key|
28
+ Permissions.invalidate_cache(cache_key)
29
+ # TODO: HANDLE LOGGER
30
+ # "info","invalidate cache {MESSAGE_CACHE_KEYS[event.type]} for event {event.type}"
31
+ end
32
+ # TODO: HANDLE LOGGER add else
33
+ # "info", "SSECacheInvalidation: unhandled message from server: {event.type}"
34
+ end
35
+ end
36
+ rescue StandardError
37
+ raise ForestException, 'Failed to reach SSE data from ForestAdmin server.'
38
+ # TODO: HANDLE LOGGER
39
+ # "debug", "SSE connection to forestadmin server due to ..."
40
+ # "warning", "SSE connection to forestadmin server closed unexpectedly, retrying."
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ class ContextVariables
4
+ attr_reader :team, :user, :request_context_variables
5
+
6
+ USER_VALUE_PREFIX = 'currentUser.'.freeze
7
+
8
+ USER_VALUE_TAG_PREFIX = 'currentUser.tags.'.freeze
9
+
10
+ USER_VALUE_TEAM_PREFIX = 'currentUser.team.'.freeze
11
+
12
+ def initialize(team, user, request_context_variables = nil)
13
+ @team = team.transform_keys(&:to_sym)
14
+ @user = user.transform_keys(&:to_sym)
15
+ @request_context_variables = request_context_variables
16
+ end
17
+
18
+ def get_value(context_variable_key)
19
+ return get_current_user_data(context_variable_key) if context_variable_key.start_with?(USER_VALUE_PREFIX)
20
+
21
+ request_context_variables[context_variable_key]
22
+ end
23
+
24
+ private
25
+
26
+ def get_current_user_data(context_variable_key)
27
+ if context_variable_key.start_with?(USER_VALUE_TEAM_PREFIX)
28
+ return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..].to_sym]
29
+ end
30
+
31
+ if context_variable_key.start_with?(USER_VALUE_TAG_PREFIX)
32
+ return user[:tags][context_variable_key[USER_VALUE_TAG_PREFIX.length..]]
33
+ end
34
+
35
+ user[context_variable_key[USER_VALUE_PREFIX.length..].to_sym]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ class ContextVariablesInjector
4
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes
5
+
6
+ def self.inject_context_in_value(value, context_variables)
7
+ inject_context_in_value_custom(value) do |context_variable_key|
8
+ context_variables.get_value(context_variable_key).to_s
9
+ end
10
+ end
11
+
12
+ def self.inject_context_in_value_custom(value)
13
+ return value unless value.is_a?(String)
14
+
15
+ value_with_context_variables_injected = value
16
+ regex = /{{([^}]+)}}/
17
+ encountered_variables = []
18
+
19
+ while (match = regex.match(value_with_context_variables_injected))
20
+ context_variable_key = match[1]
21
+
22
+ unless encountered_variables.include?(context_variable_key)
23
+ value_with_context_variables_injected.gsub!(
24
+ /{{#{context_variable_key}}}/,
25
+ yield(context_variable_key)
26
+ )
27
+ end
28
+
29
+ encountered_variables.push(context_variable_key)
30
+ end
31
+
32
+ value_with_context_variables_injected
33
+ end
34
+
35
+ def self.inject_context_in_filter(filter, context_variables)
36
+ return nil unless filter
37
+
38
+ if filter.is_a?(ConditionTreeBranch)
39
+ return ConditionTreeBranch.new(
40
+ filter.aggregator,
41
+ filter.conditions.map { |condition| inject_context_in_filter(condition, context_variables) }
42
+ )
43
+ end
44
+
45
+ ConditionTreeLeaf.new(
46
+ filter.field,
47
+ filter.operator,
48
+ inject_context_in_value(filter.value, context_variables)
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,5 @@
1
1
  require 'jwt'
2
+ require 'active_support'
2
3
  require 'active_support/time'
3
4
 
4
5
  module ForestAdminAgent
@@ -7,7 +7,7 @@ module ForestAdminAgent
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "forest-rails"
9
9
 
10
- LIANA_VERSION = "1.0.0-beta.21"
10
+ LIANA_VERSION = "1.0.0-beta.23"
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.21"
2
+ VERSION = "1.0.0-beta.23"
3
3
  end
@@ -3,6 +3,7 @@ require 'zeitwerk'
3
3
 
4
4
  loader = Zeitwerk::Loader.for_gem
5
5
  loader.inflector.inflect('oauth2' => 'OAuth2')
6
+ loader.inflector.inflect('sse_cache_invalidation' => 'SSECacheInvalidation')
6
7
  loader.setup
7
8
 
8
9
  module ForestAdminAgent