moesif_rack 1.4.19 → 2.0.1

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