moesif_rack 1.4.19 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,480 @@
1
+ require 'moesif_api'
2
+ require 'json'
3
+ require 'time'
4
+ require 'zlib'
5
+ require 'stringio'
6
+ require_relative './moesif_helpers'
7
+ require_relative './regex_config_helper'
8
+
9
+ # rule refereence
10
+ # {
11
+ # "_id": "649b64ea96d5e2384e3cece6",
12
+ # "created_at": "2023-06-27T22:38:34.405",
13
+ # "type": "regex",
14
+ # "state": 2,
15
+ # "org_id": "688:25",
16
+ # "app_id": "768:74",
17
+ # "name": "teset govern rule. ",
18
+ # "block": true,
19
+ # "applied_to": "matching",
20
+ # "applied_to_unidentified": false,
21
+ # "response": {
22
+ # "status": 205,
23
+ # "headers": {
24
+ # "X-Test": "12423"
25
+ # },
26
+ # "body": {
27
+ # "hello": "there"
28
+ # }
29
+ # },
30
+ # "regex_config": [
31
+ # {
32
+ # "conditions": [
33
+ # {
34
+ # "path": "request.route",
35
+ # "value": "test"
36
+ # },
37
+ # {
38
+ # "path": "request.verb",
39
+ # "value": "test"
40
+ # }
41
+ # ]
42
+ # },
43
+ # {
44
+ # "conditions": [
45
+ # {
46
+ # "path": "request.ip_address",
47
+ # "value": "teset"
48
+ # },
49
+ # {
50
+ # "path": "request.verb",
51
+ # "value": "5"
52
+ # }
53
+ # ]
54
+ # }
55
+ # ]
56
+ # }
57
+
58
+ # user rule reference.
59
+
60
+ # {
61
+ # "_id": "649b65d83a5a0131fd035427",
62
+ # "created_at": "2023-06-27T22:42:32.301",
63
+ # "type": "user",
64
+ # "state": 2,
65
+ # "org_id": "688:25",
66
+ # "app_id": "768:74",
67
+ # "name": "test user rule",
68
+ # "block": false,
69
+ # "applied_to": "matching",
70
+ # "applied_to_unidentified": true,
71
+ # "response": {
72
+ # "status": 200,
73
+ # "headers": {
74
+ # "teset": "test"
75
+ # }
76
+ # },
77
+ # "cohorts": [
78
+ # {
79
+ # "id": "645c3793cba73323bb0760e6"
80
+ # }
81
+ # ],
82
+ # "regex_config": [
83
+ # {
84
+ # "conditions": [
85
+ # {
86
+ # "path": "request.verb",
87
+ # "value": "get"
88
+ # }
89
+ # ]
90
+ # }
91
+ # ]
92
+ # }
93
+
94
+ module RULE_TYPES
95
+ USER = 'user'
96
+ COMPANY = 'company'
97
+ REGEX = 'regex'
98
+ end
99
+
100
+ class GovernanceRules
101
+ def initialize(debug)
102
+ @debug = debug
103
+ @moesif_helpers = MoesifHelpers.new(debug)
104
+ @regex_config_helper = RegexConfigHelper.new(debug)
105
+ @last_fetch = Time.at(0)
106
+ end
107
+
108
+ def load_rules(api_controller)
109
+ # Get Application Config
110
+ @last_fetch = Time.now.utc
111
+ @moesif_helpers.log_debug('starting downlaoding rules')
112
+ rules, _context = api_controller.get_rules
113
+
114
+ generate_rules_caching(rules)
115
+ rescue MoesifApi::APIException => e
116
+ if e.response_code.between?(401, 403)
117
+ @moesif_helpers.log_debug 'Unauthorized access getting application configuration. Please check your Appplication Id.'
118
+ end
119
+ @moesif_helpers.log_debug 'Error getting application configuration, with status code:'
120
+ @moesif_helpers.log_debug e.response_code
121
+ rescue StandardError => e
122
+ @moesif_helpers.log_debug e.to_s
123
+ end
124
+
125
+ def generate_rules_caching(rules)
126
+ @rules = rules
127
+ @regex_rules = []
128
+ @user_rules = {}
129
+ @unidentified_user_rules = []
130
+ @company_rules = {}
131
+ @unidentified_company_rules = []
132
+ if !rules.nil? && !rules.empty?
133
+ rules.each do |rule|
134
+ rule_id = rule['_id']
135
+ case rule['type']
136
+ when RULE_TYPES::USER
137
+ @user_rules[rule_id] = rule
138
+ @unidentified_user_rules.push(rule) if rule.fetch('applied_to_unidentified', false)
139
+ when RULE_TYPES::COMPANY
140
+ @company_rules[rule_id] = rule
141
+ @unidentified_company_rules.push(rule) if rule.fetch('applied_to_unidentified', false)
142
+ when RULE_TYPES::REGEX
143
+ @regex_rules.push(rule)
144
+ else
145
+ @moesif_helpers.log_debug 'rule type not found for id ' + rule_id
146
+ end
147
+ end
148
+ end
149
+ # @moesif_helpers.log_debug('user_rules processed ' + @user_rules.to_s)
150
+ # @moesif_helpers.log_debug('unidentified_user_rules' + @unidentified_user_rules.to_s);
151
+ # @moesif_helpers.log_debug('regex_rules' + @regex_rules.to_s);
152
+ rescue StandardError => e
153
+ @moesif_helpers.log_debug e.to_s
154
+ end
155
+
156
+ def has_rules
157
+ return false if @rules.nil?
158
+
159
+ @rules.length >= 1
160
+ end
161
+
162
+ # TODO
163
+ def convert_uri_to_route(uri)
164
+ # TODO: for now just return uri
165
+ uri
166
+ end
167
+
168
+ def prepare_request_fields_based_on_regex_config(_env, event_model, request_body)
169
+ operation_name = request_body.fetch('operationName', nil) unless request_body.nil?
170
+ {
171
+ 'request.verb' => event_model.request.verb,
172
+ 'request.ip_address' => event_model.request.ip_address,
173
+ 'request.route' => convert_uri_to_route(event_model.request.uri),
174
+ 'request.body.operationName' => operation_name
175
+ }
176
+ end
177
+
178
+ def get_field_value_for_path(path, request_fields, request_body)
179
+ if path && path.start_with?('request.body.') && request_body
180
+ body_key = path.sub('request.body.', '')
181
+ return request_body.fetch(body_key, nil)
182
+ end
183
+ request_fields.fetch(path, nil)
184
+ end
185
+
186
+ def check_request_with_regex_match(regex_configs, request_fields, request_body)
187
+ # since there is no regex config, customer must only care about cohort criteria, we assume regex matched
188
+ return true if regex_configs.nil? || regex_configs.empty?
189
+
190
+ array_to_or = regex_configs.map do |or_group_of_regex_rule|
191
+ conditions = or_group_of_regex_rule.fetch('conditions', [])
192
+
193
+ conditions.reduce(true) do |all_match, condition|
194
+ return false unless all_match
195
+
196
+ path = condition.fetch('path', nil)
197
+
198
+ field_value = get_field_value_for_path(path, request_fields, request_body)
199
+ reg_ex = Regexp.new condition.fetch('value', nil)
200
+
201
+ if path.nil? || field_value.nil? || reg_ex.nil?
202
+ false
203
+ else
204
+ field_value =~ reg_ex
205
+ end
206
+ end
207
+ end
208
+
209
+ array_to_or.reduce(false) { |anysofar, curr| anysofar || curr }
210
+ rescue StandardError => e
211
+ @moesif_helpers.log_debug('checking regex failed, possible malformed regex ' + e.to_s)
212
+ false
213
+ end
214
+
215
+ def get_applicable_regex_rules(request_fields, request_body)
216
+ @regex_rules.select do |rule|
217
+ regex_configs = rule['regex_config']
218
+ @moesif_helpers.log_debug('checking regex_configs')
219
+ @moesif_helpers.log_debug(regex_configs.to_s)
220
+ if regex_configs.nil?
221
+ true
222
+ else
223
+ @moesif_helpers.log_debug('checking regex_configs')
224
+ @moesif_helpers.log_debug(regex_configs.to_s)
225
+ check_request_with_regex_match(regex_configs, request_fields, request_body)
226
+ end
227
+ end
228
+ end
229
+
230
+ def get_applicable_user_rules_for_unidentified_user(request_fields, request_body)
231
+ @unidentified_user_rules.select do |rule|
232
+ regex_matched = check_request_with_regex_match(rule.fetch('regex_config', nil), request_fields, request_body)
233
+
234
+ @moesif_helpers.log_debug('regexmatched for unidetnfied_user rule ' + rule.to_json) if regex_matched
235
+ regex_matched
236
+ end
237
+ end
238
+
239
+ def get_applicable_user_rules(request_fields, request_body, config_user_rules_values)
240
+ applicable_rules_list = []
241
+
242
+ rule_ids_hash_that_is_in_cohort = {}
243
+
244
+ # handle uses where user_id is in ARLEADY in the cohort of the rules.
245
+ # if user is in a cohorot of the rule, it will come from config user rule values array, which is
246
+ # config.user_rules.user_id.[]
247
+ unless config_user_rules_values.nil?
248
+ config_user_rules_values.each do |entry|
249
+ rule_id = entry['rules']
250
+ # this is user_id matched cohort set in the rule.
251
+ rule_ids_hash_that_is_in_cohort[rule_id] = true unless rule_id.nil?
252
+ # rule_ids_hash_that_I_am_in_cohot{629847be77e75b13635aa868: true}
253
+
254
+ found_rule = @user_rules[rule_id]
255
+ if found_rule.nil?
256
+ @moesif_helpers.log_debug('rule for not foun for ' + rule_id.to_s)
257
+ next
258
+ end
259
+
260
+ @moesif_helpers.log_debug('found rule in cached user rules' + rule_id)
261
+
262
+ regex_matched = check_request_with_regex_match(found_rule.fetch('regex_config', nil), request_fields,
263
+ request_body)
264
+
265
+ unless regex_matched
266
+ @moesif_helpers.log_debug('regex not matched, skipping ' + rule_id.to_s)
267
+ next
268
+ end
269
+
270
+ if found_rule['applied_to'] == 'not_matching'
271
+ # mean not matching, i.e. we do not apply the rule since current user is in cohort.
272
+ @moesif_helpers.log_debug('applied to is not matching to users in this cohort, so skipping add this rule')
273
+ else
274
+ # since applied_to is matching, we are in the cohort, we apply the rule by adding it to the list.
275
+ @moesif_helpers.log_debug('applied to is matching' + found_rule['applied_to'])
276
+ applicable_rules_list.push(found_rule)
277
+ end
278
+ end
279
+ end
280
+
281
+ # now user id is NOT associated with any cohort rule so we have to add user rules that is "Not matching"
282
+ @user_rules.each do |_rule_id, rule|
283
+ # we want to apply to any "not_matching" rules.
284
+ # we want to make sure user is not in the cohort of the rule.
285
+ next unless rule['applied_to'] == 'not_matching' && !rule_ids_hash_that_is_in_cohort[_rule_id]
286
+
287
+ regex_matched = check_request_with_regex_match(rule.fetch('regex_config', nil), request_fields, request_body)
288
+ applicable_rules_list.push(rule) if regex_matched
289
+ end
290
+
291
+ applicable_rules_list
292
+ end
293
+
294
+ def get_applicable_company_rules_for_unidentified_company(request_fields, request_body)
295
+ @unidentified_company_rules.select do |rule|
296
+ regex_matched = check_request_with_regex_match(rule.fetch('regex_config', nil), request_fields, request_body)
297
+
298
+ regex_matched
299
+ end
300
+ end
301
+
302
+ def get_applicable_company_rules(request_fields, request_body, config_company_rules_values)
303
+ applicable_rules_list = []
304
+
305
+ @moesif_helpers.log_debug('get applicable company rules for identifed company using these config values: ' + config_company_rules_values.to_s)
306
+
307
+ rule_ids_hash_that_is_in_cohort = {}
308
+
309
+ # handle where company_id is in the cohort of the rules.
310
+ unless config_company_rules_values.nil?
311
+ config_company_rules_values.each do |entry|
312
+ rule_id = entry['rules']
313
+ # this is company_id matched cohort set in the rule.
314
+ rule_ids_hash_that_is_in_cohort[rule_id] = true unless rule_id.nil?
315
+
316
+ found_rule = @company_rules[rule_id]
317
+
318
+ if found_rule.nil?
319
+ @moesif_helpers.log_debug('company rule for not found for ' + rule_id.to_s)
320
+ next
321
+ end
322
+
323
+ regex_matched = check_request_with_regex_match(found_rule.fetch('regex_config', nil), request_fields,
324
+ request_body)
325
+
326
+ unless regex_matched
327
+ @moesif_helpers.log_debug('regex not matched, skipping ' + rule_id.to_s)
328
+ next
329
+ end
330
+
331
+ if found_rule['applied_to'] == 'not_matching'
332
+ # mean not matching, i.e. we do not apply the rule since current user is in cohort.
333
+ @moesif_helpers.log_debug('applied to is companies not in this cohort, so skipping add this rule')
334
+ else
335
+ # since applied_to is matching, we are in the cohort, we apply the rule by adding it to the list.
336
+ @moesif_helpers.log_debug('applied to is matching' + found_rule['applied_to'])
337
+ applicable_rules_list.push(found_rule)
338
+ end
339
+ end
340
+ end
341
+
342
+ # handle is NOT in the cohort of rule so we have to apply rules that are "Not matching"
343
+ @company_rules.each do |_rule_id, rule|
344
+ # we want to apply to any "not_matching" rules.
345
+ next unless rule['applied_to'] == 'not_matching' && !rule_ids_hash_that_is_in_cohort[_rule_id]
346
+
347
+ regex_matched = check_request_with_regex_match(rule.fetch('regex_config', nil), request_fields, request_body)
348
+ applicable_rules_list.push(rule) if regex_matched
349
+ end
350
+ applicable_rules_list
351
+ end
352
+
353
+ def replace_merge_tag_values(template_obj_or_val, mergetag_values, variables_from_rules)
354
+ # take the template, either headers or body, and replace with mergetag_values
355
+ # recursively
356
+ return template_obj_or_val if variables_from_rules.nil? || variables_from_rules.empty?
357
+
358
+ if template_obj_or_val.nil?
359
+ template_obj_or_val
360
+ elsif template_obj_or_val.is_a?(String)
361
+ temp_val = template_obj_or_val
362
+ variables_from_rules.each do |variable|
363
+ variable_name = variable['name']
364
+ variable_value = mergetag_values.nil? ? 'UNKNOWN' : mergetag_values.fetch(variable_name, 'UNKNOWN')
365
+ temp_val = temp_val.sub('{{' + variable_name + '}}', variable_value)
366
+ end
367
+ temp_val
368
+ elsif template_obj_or_val.is_a?(Array)
369
+ tempplate_obj_or_val.map { |entry| replace_merge_tag_values(entry, mergetag_values, variables_from_rules) }
370
+ elsif template_obj_or_val.is_a?(Hash)
371
+ result_hash = {}
372
+ template_obj_or_val.each do |key, entry|
373
+ result_hash[key] = replace_merge_tag_values(entry, mergetag_values, variables_from_rules)
374
+ end
375
+ result_hash
376
+ else
377
+ template_obj_or_val
378
+ end
379
+ end
380
+
381
+ def modify_response_for_applicable_rule(rule, response, mergetag_values)
382
+ # For matched rule, we can now modify the response
383
+ # response is a hash with :status, :headers and :body or nil
384
+ @moesif_helpers.log_debug('about to modify response ' + mergetag_values.to_s)
385
+ new_headers = response[:headers].clone
386
+ rule_variables = rule['variables']
387
+ # headers are always merged togethe
388
+ rule_headers = replace_merge_tag_values(rule.dig('response', 'headers'), mergetag_values, rule_variables)
389
+ # insersion of rule headers, we do not replace all headers.
390
+ rule_headers.each { |key, entry| new_headers[key] = entry } unless rule_headers.nil?
391
+
392
+ response[:headers] = new_headers
393
+
394
+ # only replace status and body if it is blocking.
395
+ if rule['block']
396
+ @moesif_helpers.log_debug('rule is block' + rule.to_s)
397
+ response[:status] = rule.dig('response', 'status') || response[:status]
398
+ new_body = replace_merge_tag_values(rule.dig('response', 'body'), mergetag_values, rule_variables)
399
+ response[:body] = new_body
400
+ response[:block_rule_id] = rule['_id']
401
+ end
402
+
403
+ response
404
+ rescue StandardError => e
405
+ @moesif_helpers.log_debug('failed to apply rule ' + rule.to_json + ' for ' + response.to_s + ' error: ' + e.to_s)
406
+ response
407
+ end
408
+
409
+ def apply_rules_list(applicable_rules, response, config_rule_values)
410
+ return response if applicable_rules.nil? || applicable_rules.empty?
411
+
412
+ # config_rule_values if exists should be from config for a particular user for an list of rules.
413
+ # [
414
+ # {
415
+ # "rules": "629847be77e75b13635aa868",
416
+ # "values": {
417
+ # "1": "viewer-Float(online)-nd",
418
+ # "2": "[\"62984b17715c450ba6ad46b2\"]",
419
+ # "3": "[\"user cohort created at 6/1, 10:31 PM\"]",
420
+ # "4": "viewer-Float(online)-nd"
421
+ # }
422
+ # }
423
+ # ]
424
+
425
+ applicable_rules.reduce(response) do |prev_response, rule|
426
+ unless config_rule_values.nil?
427
+ found_rule_value_pair = config_rule_values.find { |rule_value_pair| rule_value_pair['rules'] == rule['_id'] }
428
+ mergetag_values = found_rule_value_pair['values'] unless found_rule_value_pair.nil?
429
+ end
430
+ modify_response_for_applicable_rule(rule, prev_response, mergetag_values)
431
+ end
432
+ end
433
+
434
+ def govern_request(config, env, event_model, status, headers, body)
435
+ # we can skip if rules does not exist or config does not exist
436
+ return if @rules.nil? || @rules.empty?
437
+
438
+ if event_model
439
+ request_body = event_model.request.body
440
+ request_fields = prepare_request_fields_based_on_regex_config(env, event_model, request_body)
441
+ end
442
+
443
+ user_id = event_model.user_id
444
+ company_id = event_model.company_id
445
+
446
+ # apply in reverse order of priority.
447
+ # Priority is user rule, company rule, and regex.
448
+ # by apply in reverse order, the last rule become highest priority.
449
+
450
+ new_response = {
451
+ status: status,
452
+ headers: headers,
453
+ body: body
454
+ }
455
+
456
+ applicable_regex_rules = get_applicable_regex_rules(request_fields, request_body)
457
+ new_response = apply_rules_list(applicable_regex_rules, new_response, nil)
458
+
459
+ if company_id.nil?
460
+ company_rules = get_applicable_company_rules_for_unidentified_company(request_fields, request_body)
461
+ new_response = apply_rules_list(company_rules, new_response, nil)
462
+ else
463
+ config_rule_values = config.dig('company_rules', company_id) unless config.nil?
464
+ company_rules = get_applicable_company_rules(request_fields, request_body, config_rule_values)
465
+ new_response = apply_rules_list(company_rules, new_response, config_rule_values)
466
+ end
467
+
468
+ if user_id.nil?
469
+ user_rules = get_applicable_user_rules_for_unidentified_user(request_fields, request_body)
470
+ new_response = apply_rules_list(user_rules, new_response, nil)
471
+ else
472
+ config_rule_values = config.dig('user_rules', user_id) unless config.nil?
473
+ user_rules = get_applicable_user_rules(request_fields, request_body, config_rule_values)
474
+ new_response = apply_rules_list(user_rules, new_response, config_rule_values)
475
+ end
476
+ new_response
477
+ rescue StandardError => e
478
+ @moesif_helpers.log_debug 'error try to govern request:' + e.to_s + 'for event' + event_model.to_s
479
+ end
480
+ end
@@ -1,14 +1,41 @@
1
1
  require 'time'
2
+ require 'rack'
2
3
 
3
4
  class MoesifHelpers
5
+ def initialize(debug)
6
+ @debug = debug
7
+ end
4
8
 
5
- def initialize debug
6
- @debug = debug
9
+ def log_debug(message)
10
+ return unless @debug
11
+
12
+ puts("#{Time.now} [Moesif Middleware] PID #{Process.pid} TID #{Thread.current.object_id} #{message}")
13
+ end
14
+
15
+ def format_replacement_body(replacement_body, original_body)
16
+ # replacement_body is an hash or array json in this case.
17
+ # but original body could be in chunks already. we want to follow suit.
18
+ return original_body if replacement_body.nil?
19
+
20
+ if original_body.instance_of?(Hash) || original_body.instance_of?(Array)
21
+ log_debug 'original_body is a hash or array return as is'
22
+ return replacement_body
7
23
  end
8
24
 
9
- def log_debug(message)
10
- if @debug
11
- puts("#{Time.now.to_s} [Moesif Middleware] PID #{Process.pid} TID #{Thread.current.object_id} #{message}")
12
- end
25
+ if original_body.is_a? String
26
+ log_debug 'original_body is a string, return a string format'
27
+ return replacement_body.to_json.to_s
13
28
  end
29
+
30
+ if original_body.respond_to?(:each) && original_body.respond_to?(:inject)
31
+ # we know it is an chunks
32
+ log_debug 'original_body respond to iterator, must likely chunks'
33
+ [replacement_body.to_json.to_s]
34
+ end
35
+
36
+ [replacement_body.to_json.to_s]
37
+ rescue StandardError => e
38
+ log_debug 'failed to convert replacement body ' + e.to_s
39
+ [replacement_body.to_json.to_s]
40
+ end
14
41
  end