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.
- checksums.yaml +4 -4
- data/forest_admin_agent.gemspec +3 -1
- data/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +1 -1
- data/lib/forest_admin_agent/auth/oidc_client_manager.rb +3 -2
- data/lib/forest_admin_agent/builder/agent_factory.rb +7 -9
- data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +1 -1
- data/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +14 -0
- data/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +14 -0
- data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +1 -1
- data/lib/forest_admin_agent/http/Exceptions/require_approval.rb +15 -0
- data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +32 -4
- data/lib/forest_admin_agent/http/router.rb +1 -0
- data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +1 -0
- data/lib/forest_admin_agent/routes/charts/charts.rb +214 -0
- data/lib/forest_admin_agent/routes/resources/count.rb +7 -4
- data/lib/forest_admin_agent/routes/resources/delete.rb +5 -1
- data/lib/forest_admin_agent/routes/resources/list.rb +9 -3
- data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +10 -1
- data/lib/forest_admin_agent/routes/resources/related/count_related.rb +2 -1
- data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +2 -0
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +9 -1
- data/lib/forest_admin_agent/routes/resources/related/update_related.rb +8 -1
- data/lib/forest_admin_agent/routes/resources/show.rb +5 -3
- data/lib/forest_admin_agent/routes/resources/store.rb +11 -10
- data/lib/forest_admin_agent/routes/resources/update.rb +4 -3
- data/lib/forest_admin_agent/serializer/forest_chart_serializer.rb +19 -0
- data/lib/forest_admin_agent/services/permissions.rb +268 -0
- data/lib/forest_admin_agent/services/smart_action_checker.rb +95 -0
- data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +45 -0
- data/lib/forest_admin_agent/utils/context_variables.rb +39 -0
- data/lib/forest_admin_agent/utils/context_variables_injector.rb +53 -0
- data/lib/forest_admin_agent/utils/query_string_parser.rb +1 -0
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- data/lib/forest_admin_agent.rb +1 -0
- 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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|