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

Sign up to get free protection for your applications and to get access to all the features.
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