moesif_rack 1.4.19 → 1.5.1

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