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