pg_ddm_sql_modifier 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 @@
1
+ # pg_ddm_sql_modifier
@@ -0,0 +1,23 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/extensiontask'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+
6
+ Rake::ExtensionTask.new 'pg_ddm_sql_modifier' do |ext|
7
+ ext.lib_dir = 'lib/pg_ddm_sql_modifier'
8
+ end
9
+
10
+ RSpec::Core::RakeTask.new
11
+ RuboCop::RakeTask.new
12
+
13
+ task spec: :compile
14
+
15
+ task default: %i[lint spec]
16
+ task test: :spec
17
+ task lint: :rubocop
18
+
19
+ task :clean do
20
+ FileUtils.rm_rf File.join(__dir__, 'tmp/')
21
+ FileUtils.rm_f Dir.glob(File.join(__dir__, 'ext/pg_query/*.o'))
22
+ FileUtils.rm_f File.join(__dir__, 'lib/pg_query/pg_ddm_sql_modifier.bundle')
23
+ end
@@ -0,0 +1,3 @@
1
+ require 'pg_ddm_sql_modifier/version'
2
+ require 'pg_ddm_sql_modifier/parser'
3
+
@@ -0,0 +1,498 @@
1
+ require 'pg_query'
2
+ require 'json'
3
+ require 'etcdv3'
4
+ require 'hashie'
5
+
6
+ class PgQueryOpt
7
+ @sql = nil
8
+ @query_parser = nil
9
+ @table_list = []
10
+ @remove_ref = 0
11
+ @etcd = nil
12
+ @etcd_host = 'localhost'
13
+ @etcd_port = '2379'
14
+ @etcd_user = nil
15
+ @etcd_passwd = nil
16
+ @pass_tag = ''
17
+ @tag_regex = ''
18
+ @data_in_etcd = {}
19
+ @col_list = {}
20
+ @ref = nil
21
+ @groups = {}
22
+ @return_column_ref = {}
23
+ @in_function = 0
24
+ @name = nil
25
+ @mask = true
26
+ @change_name = nil
27
+ @mode = 'select'
28
+
29
+ def self.hi
30
+ puts "Hello world!"
31
+ end
32
+
33
+ def properties(sql, username, db, etcd_host, etcd_port, etcd_user, etcd_passwd, user_regex, tag_regex, default_scheme, tag_users)
34
+
35
+ @sql = sql
36
+ @username = username
37
+ @db = db
38
+ @etcd_host = etcd_host
39
+ @etcd_port = etcd_port
40
+ @etcd_user = etcd_user
41
+ @etcd_passwd = etcd_passwd
42
+ @user_regex = user_regex
43
+ @tag_regex = tag_regex
44
+ @data_in_etcd = {}
45
+ @default_scheme = default_scheme
46
+ @in_function = 0
47
+ @name = nil
48
+ @mask = true
49
+ @change_name = nil
50
+ @mode = 'select'
51
+
52
+
53
+ tag_users = tag_users.delete(' ').split(',')
54
+ if tag_users.include?(username)
55
+ @pass_tag = /#{@tag_regex}/.match(@sql)
56
+ return @sql if @pass_tag
57
+ end
58
+
59
+ conn_etcd
60
+
61
+ @query_parser = PgQuery.parse(@sql)
62
+ @sql = @sql.strip
63
+
64
+ @tag_sql = %r{(?<=^/\*)([^\*]*)(?=\*/)}.match(@sql)
65
+ @tag_sql = @tag_sql ? '/* ' + @tag_sql[1].strip + ' */' : ''
66
+
67
+ if @user_id.nil?
68
+ @user_id = /#{@user_regex}/.match(@sql)
69
+
70
+ @user_id = @user_id[1].strip if @user_id
71
+ end
72
+
73
+ user_to_group
74
+
75
+ return @sql if @groups.empty?
76
+
77
+ i = 0
78
+ @query_parser.tree.each do |parse_item|
79
+ @query_parser.tree[i] = parse(parse_item)
80
+ i += 1
81
+ end
82
+ @tag_sql + @query_parser.deparse
83
+ rescue StandardError => e
84
+ puts e
85
+ puts e.backtrace.to_s
86
+ @sql
87
+
88
+ end
89
+
90
+ def get_role(sql)
91
+ parser = if @sql
92
+ @query_parser
93
+ else
94
+ PgQuery.parse(sql)
95
+ end
96
+ tree = parser.tree
97
+ tree.extend Hashie::Extensions::DeepFind
98
+ keys = tree.deep_find_all('FuncCall')
99
+ keys2 = tree.deep_find_all('TransactionStmt')
100
+ keys3 = tree.deep_find_all('SelectStmt')
101
+
102
+
103
+ if keys.nil? && keys2.nil? && !keys3.nil?
104
+ 'read'
105
+ else
106
+ 'master'
107
+ end
108
+ end
109
+
110
+ def conn_etcd
111
+ return unless @etcd.nil?
112
+
113
+ @etcd = if @etcd_user.empty?
114
+ Etcdv3.new(endpoints: 'http://' + @etcd_host + ':' + @etcd_port, command_timeout: 5)
115
+ else
116
+ Etcdv3.new(endpoints: 'http://' + @etcd_host + ':' + @etcd_port, command_timeout: 5, user: @etcd_user, password: @etcd_passwd)
117
+ end
118
+ end
119
+
120
+ def user_to_group
121
+ data = []
122
+ @groups = {}
123
+ key_replace = []
124
+
125
+ unless @username.nil?
126
+ filter = '/dbuser/' + @username.strip
127
+ key_replace.push(filter)
128
+ data += @etcd.get(filter, range_end: filter + '0').kvs
129
+ end
130
+
131
+ unless @user_id.nil?
132
+ filter = '/users/' + @user_id.strip
133
+ key_replace.push(filter)
134
+ data += @etcd.get(filter, range_end: filter + '0').kvs
135
+ end
136
+
137
+ filter = '/users/*'
138
+ key_replace.push(filter)
139
+ data += @etcd.get(filter, range_end: filter + '0').kvs
140
+
141
+ filter = '/dbuser/*'
142
+ key_replace.push(filter)
143
+ data += @etcd.get(filter, range_end: filter + '0').kvs
144
+
145
+
146
+ i = 0
147
+ data.each do |val|
148
+ val_obj = JSON.parse(val.value)
149
+ if val_obj['enabled'].to_s == 'false'
150
+ data.delete_at(i)
151
+ else
152
+ key = val.key.dup
153
+ key_replace.each { |key_data| key.gsub! key_data, '' }
154
+ @groups[key] = {}
155
+ end
156
+ i += 1
157
+ end
158
+
159
+ data
160
+ end
161
+
162
+ def etcd_data(filter_id)
163
+ if @data_in_etcd[filter_id].nil?
164
+ data = @etcd.get(filter_id, range_end: filter_id + '0')
165
+ @data_in_etcd[filter_id] = data
166
+ end
167
+ return {} if @data_in_etcd[filter_id].kvs.count.zero?
168
+
169
+ JSON.parse(@data_in_etcd[filter_id].kvs.first.value)
170
+
171
+ end
172
+
173
+ def get_string(node)
174
+ return node if node.is_a?(String)
175
+ return node['String']['str'] unless node['String'].nil?
176
+ return nil unless node['A_Star'].nil?
177
+
178
+ node.to_s
179
+ end
180
+
181
+ def get_alias(node)
182
+ if node.is_a?(Array)
183
+ alias_array = []
184
+ node.each do |col|
185
+ alias_array.push(get_string(col))
186
+ end
187
+ alias_array.join('.')
188
+ else
189
+ get_string(node)
190
+ end
191
+ end
192
+
193
+ def get_col_with_table(schema, table)
194
+ etcd_data('/' + @db + '/' + schema + '/' + table)
195
+ end
196
+
197
+ # @param [Object] ref
198
+ # @param [Object] table_list
199
+ # @return [Hash{null->null}]
200
+ def mask(ref, table_list)
201
+ return_column_ref = {}
202
+ if ref.count == 1
203
+ table_list.each do |alias_name, table|
204
+ next unless table['columns'].find { |col| !col.key(get_string(ref[-1])).nil? }
205
+
206
+ ref = alias_name.split('.') + [get_string(ref[-1])]
207
+ break
208
+ end
209
+ end
210
+
211
+ tab = table_list[get_alias(ref.first(ref.count - 1))] unless table_list.empty?
212
+ unless tab.nil?
213
+ filter = '/rules/' + @db + '/' + tab['schema'] + '/' + tab['table']
214
+ data = {}
215
+ @groups.each do |key, value|
216
+ group = etcd_data(key)
217
+ next if group['enabled'].to_s == 'false'
218
+
219
+ data_temp = etcd_data(filter + '/' + get_string(ref[-1]) + key)
220
+ next if data_temp['enabled'].to_s == 'false'
221
+
222
+ if data_temp.empty?
223
+ data = {}
224
+ else
225
+ data = data_temp
226
+ break
227
+ end
228
+ end
229
+
230
+ unless data.empty?
231
+ name = if @name.nil?
232
+ get_string(ref[-1])
233
+ elsif @name.is_a?(String)
234
+ @name
235
+ end
236
+
237
+ if data['rule'] == 'send_null'
238
+ return_column_ref = { 'ResTarget' => { 'name' => name, 'val' => { 'A_Const' => { 'val' => { 'Null' => {} } } } } }
239
+ elsif data['rule'] == 'delete_col'
240
+ return_column_ref = { 'del' => 1 }
241
+ else
242
+ change_colname = JSON.parse(data['prop'].gsub('%col%', { 'ColumnRef' => { 'fields' => ref } }.to_json))
243
+ # TODO: Schema is not dynamic
244
+ func = { 'funcname' => [{ 'String' => { 'str' => 'mask' } }, { 'String' => { 'str' => data['rule'] } }], 'args' => change_colname }
245
+
246
+ return_column_ref = { 'ResTarget' => { 'name' => name, 'val' => { 'FuncCall' => func } } }
247
+ end
248
+ if data['filter'] != ''
249
+ filter_tables = {}
250
+ filter_tables[tab['alias']] = tab
251
+
252
+ filter_where = get_filters(filter_tables, data)
253
+ return_column_ref = { 'ResTarget' => { 'name' => name, 'val' => { 'CaseExpr' => { 'args' => [{ 'CaseWhen' => { 'expr' => filter_where[0], 'result' => return_column_ref['ResTarget']['val'] } }], 'defresult' => { 'ColumnRef' => { 'fields' => ref } } } } } }
254
+ end
255
+ end
256
+ end
257
+ return_column_ref
258
+ end
259
+
260
+ # @param [Object] table_list
261
+ # @param [Object] where_part
262
+ def get_filters(table_list, where_part = nil)
263
+ filter_w = []
264
+ table_list.each do |alias_name, table|
265
+ filter_list = []
266
+ next if table['schema'].nil?
267
+
268
+ if where_part.nil?
269
+ @groups.each do |key, value|
270
+ filter_temp = etcd_data('/sqlfilter/' + @db + '/' + table['schema'] + '/' + table['table'] + key)
271
+ next if filter_temp.empty?
272
+ next if filter_temp['enabled'].to_s == 'false'
273
+
274
+ filter_list.push(filter_temp)
275
+ end
276
+ filter_temp = etcd_data('/sqlfilter/' + @db + '/' + table['schema'] + '/' + table['table'] + '/*')
277
+
278
+ unless filter_temp.empty?
279
+ filter_list.push(filter_temp) if filter_temp['enabled'].to_s == 'true'
280
+ end
281
+ else
282
+ next unless (@db + '.' + table['schema'] + '.' + table['table']) == where_part['table_column'].split('.')[0...-1].join('.')
283
+
284
+ filter_list.push(where_part)
285
+ end
286
+ next if filter_list.count.zero?
287
+
288
+ filter_list.each do |filter|
289
+ where_condition = filter['filter'].gsub(table['schema'] + '.' + table['table'], alias_name)
290
+ where_query = PgQuery.parse('SELECT WHERE ' + where_condition)
291
+ filter_w.push(where_query.tree[0]['RawStmt']['stmt']['SelectStmt']['whereClause'])
292
+ end
293
+ end
294
+ filter_w
295
+ end
296
+
297
+ # @param [Object] items
298
+ # @param [Array] table_list
299
+ def parse(items, table_list = [], masked = true)
300
+ old_table_list = []
301
+ return if items.nil?
302
+
303
+ if items.is_a?(Hash)
304
+ if items.keys.is_a?(Array)
305
+ items.keys.each do |item|
306
+ case item
307
+ when 'fields'
308
+ @ref = items[item]
309
+ @return_column_ref = mask(@ref, table_list) if @ref[-1]['A_Star'].nil? && masked == true && @mode == 'select'
310
+ @name = nil
311
+ when 'ResTarget'
312
+ if @mode == 'update' && masked == true
313
+ masked_field = mask([items[item]['name']], table_list)
314
+ items[item] = { "name" => masked_field[item]['name'], "val" => { "CaseExpr" => { "args" => [{ "CaseWhen" => { "expr" => { "A_Expr" => { "kind" => 0, "name" => [{ "String" => { "str" => "=" } }], "lexpr" => masked_field[item]['val'], "rexpr" => items[item]['val'] } }, "result" => { "ColumnRef" => { "fields" => [items[item]['name']] } } } }], "defresult" => items[item]['val'] } } } unless masked_field.empty?
315
+ end
316
+ when 'name'
317
+ @name = items[item]
318
+ when 'ColumnRef', 'args'
319
+ @remove_ref = 3
320
+ when 'funcname'
321
+ @in_function = 1 if get_string(items[item][0]) == 'count'
322
+ when 'A_Star'
323
+ if @in_function.zero?
324
+ @col_list = {}
325
+ case @ref.count
326
+ when 1
327
+ table_list.each do |alias_name, table|
328
+ @col_list[alias_name] = table['columns']
329
+ end
330
+ when 2, 3
331
+ alias_name = get_alias(@ref.first(@ref.count - 1))
332
+ @col_list[alias_name] = table_list[alias_name]['columns'] unless table_list[alias_name].nil?
333
+ else
334
+ raise 'SQL exception check your alias names (' + get_alias(@ref) + ')'
335
+ end
336
+ @remove_ref = 1
337
+ else
338
+ @in_function = 0
339
+ end
340
+ when 'SelectStmt', 'UpdateStmt'
341
+ @mode = if item == 'UpdateStmt'
342
+ 'update'
343
+ else
344
+ 'select'
345
+ end
346
+ old_table_list = table_list
347
+ table_list = find_table_list(items[item])
348
+ filters = get_filters(table_list)
349
+ when 'InsertStmt'
350
+ return items
351
+ when 'A_Expr'
352
+ if items[item].is_a?(Hash)
353
+ if items[item].include?('name')
354
+ masked = false unless get_string(items[item]['name'][0]) == '||'
355
+ end
356
+ end
357
+ end
358
+
359
+
360
+ parse(items[item], table_list, masked)
361
+
362
+ masked = true if item == 'A_Expr'
363
+ @name = nil if item == 'name'
364
+
365
+ @remove_ref = 2 if item == 'ResTarget' && @remove_ref == 1
366
+
367
+ if item == 'ResTarget'
368
+ unless items[item].include?('name')
369
+ items[item]['name'] = @change_name unless @change_name.nil?
370
+ end
371
+ @change_name = nil
372
+ end
373
+
374
+ if item == 'fields' && @remove_ref == 3 && !@return_column_ref.nil? && @return_column_ref['del'].nil?
375
+ unless @return_column_ref.empty?
376
+ items.delete(item)
377
+ items[item] = [@return_column_ref['ResTarget']['val']]
378
+ @change_name = @return_column_ref['ResTarget']['name'] if @return_column_ref['ResTarget'].include?('name')
379
+ end
380
+ @return_column_ref = {}
381
+ end
382
+
383
+ if item == 'ResTarget' && !@return_column_ref.nil?
384
+ unless @return_column_ref.empty?
385
+ if @return_column_ref['del'].nil?
386
+ items[item] = @return_column_ref['ResTarget']
387
+ else
388
+ items.delete(item)
389
+ end
390
+ end
391
+ @return_column_ref = {}
392
+ end
393
+
394
+ if !filters.nil? && item == 'SelectStmt'
395
+ if filters.count > 0
396
+ filters.push(items['SelectStmt']['whereClause']) unless items['SelectStmt']['whereClause'].nil?
397
+ items['SelectStmt']['whereClause'] = { 'BoolExpr' => { 'boolop' => 0, 'args' => filters } }
398
+ end
399
+ end
400
+
401
+ table_list = old_table_list if %w[SelectStmt UpdateStmt].include?(item)
402
+
403
+ end
404
+ end
405
+ end
406
+
407
+ if items.is_a?(Array)
408
+ i = 0
409
+ items.each do |item|
410
+ parse(item, table_list, masked)
411
+
412
+ reparse = 0
413
+ unless item.nil?
414
+ if item.empty?
415
+ items.delete_at(i)
416
+ reparse = 1
417
+ end
418
+ end
419
+
420
+ if @remove_ref == 2
421
+ k = 1
422
+
423
+ @col_list.each do |alias_name, alias_table|
424
+ next if alias_table.nil?
425
+
426
+ if alias_table.count > 0 && reparse.zero?
427
+ reparse = 1
428
+ items.delete_at(i)
429
+ k -= 1
430
+ end
431
+ alias_table.each do |col|
432
+ fields = alias_name.split('.').push(col['column_name'])
433
+ items.insert(i + k, 'ResTarget' => { 'val' => { 'ColumnRef' => { 'fields' => fields } } })
434
+ k += 1
435
+ end
436
+ end
437
+ @col_list = {}
438
+ @remove_ref = 0
439
+
440
+ end
441
+ parse(items[i], table_list, masked) if reparse == 1
442
+ unless items[i].nil?
443
+ if items[i].empty?
444
+ items.delete_at(i)
445
+ end
446
+ end
447
+ i += 1
448
+ end
449
+ end
450
+
451
+ items
452
+ end
453
+
454
+ def find_table_list(tree, table_list = {})
455
+ key_list = %w[SelectStmt UpdateStmt relation fromClause JoinExpr larg rarg]
456
+ if tree.is_a?(Array)
457
+ tree.each do |k|
458
+ find_table_list(k, table_list)
459
+ end
460
+ else
461
+ tree.keys.each do |key|
462
+ find_table_list(tree[key], table_list) if key_list.include?(key)
463
+ end
464
+
465
+ unless tree['RangeVar'].nil?
466
+ table = {}
467
+ table['table'] = tree['RangeVar']['relname']
468
+ if tree['RangeVar']['schemaname'].nil?
469
+ unless @default_scheme.nil?
470
+ @default_scheme.split(',').each do |scheme|
471
+ scheme = scheme.strip
472
+ table_defination = get_col_with_table(scheme, table['table'])
473
+ next if table_defination.empty?
474
+
475
+ table['schema'] = scheme
476
+ break
477
+ end
478
+ end
479
+ else
480
+ table['schema'] = tree['RangeVar']['schemaname']
481
+ end
482
+ table['alias'] = if tree['RangeVar']['alias'].nil?
483
+ if tree['RangeVar']['schemaname'].nil?
484
+ table['table']
485
+ else
486
+ table['schema'] + '.' + table['table']
487
+ end
488
+ else
489
+ tree['RangeVar']['alias']['Alias']['aliasname']
490
+ end
491
+
492
+ table['columns'] = get_col_with_table(table['schema'], table['table']) unless table['schema'].nil?
493
+ table_list[table['alias']] = table
494
+ end
495
+ end
496
+ table_list
497
+ end
498
+ end