db_obfuscation 0.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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +83 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/TODO +15 -0
- data/bin/console +25 -0
- data/bin/db_obfuscation +121 -0
- data/bin/obfuscation_test +54 -0
- data/cli/db_dump.rb +29 -0
- data/cli/migrator.rb +26 -0
- data/cli/seeder.rb +52 -0
- data/db_obfuscation.gemspec +25 -0
- data/features/bin/dump.feature +21 -0
- data/features/bin/obfuscation.feature +12 -0
- data/features/bin/test_database_tasks.feature +16 -0
- data/features/support.rb +1 -0
- data/lib/db_obfuscation.rb +50 -0
- data/lib/db_obfuscation/batch_formulator.rb +26 -0
- data/lib/db_obfuscation/config.rb +43 -0
- data/lib/db_obfuscation/database.rb +8 -0
- data/lib/db_obfuscation/environment.rb +14 -0
- data/lib/db_obfuscation/filtering.rb +56 -0
- data/lib/db_obfuscation/filtering/column.rb +40 -0
- data/lib/db_obfuscation/filtering/truncation.rb +18 -0
- data/lib/db_obfuscation/obfuscation_strategy.rb +22 -0
- data/lib/db_obfuscation/obfuscator.rb +65 -0
- data/lib/db_obfuscation/query_builder.rb +62 -0
- data/lib/db_obfuscation/truncation.rb +39 -0
- data/lib/db_obfuscation/util/trigger.rb +83 -0
- data/lib/db_obfuscation/version.rb +4 -0
- data/spec/cli/db_dump_spec.rb +33 -0
- data/spec/cli/migrator_spec.rb +59 -0
- data/spec/cli/seeder_spec.rb +33 -0
- data/spec/config/database.yml +5 -0
- data/spec/config/table_strategies/table_1.yml +3 -0
- data/spec/config/table_strategies/table_2.yml +4 -0
- data/spec/config/table_strategies/truncation_table_1.yml +3 -0
- data/spec/config/table_strategies/whitelisted_table_1.yml +3 -0
- data/spec/config/truncation_patterns.yml +2 -0
- data/spec/config/whitelisted_tables.yml +1 -0
- data/spec/db_obfuscation/batch_formulator_spec.rb +36 -0
- data/spec/db_obfuscation/config_spec.rb +60 -0
- data/spec/db_obfuscation/database_spec.rb +10 -0
- data/spec/db_obfuscation/filtering/column_spec.rb +82 -0
- data/spec/db_obfuscation/filtering/truncation_spec.rb +41 -0
- data/spec/db_obfuscation/filtering_spec.rb +39 -0
- data/spec/db_obfuscation/obfuscation_strategy_spec.rb +43 -0
- data/spec/db_obfuscation/obfuscator_spec.rb +150 -0
- data/spec/db_obfuscation/query_builder_spec.rb +259 -0
- data/spec/db_obfuscation/truncation_spec.rb +31 -0
- data/spec/db_obfuscation/util/trigger_spec.rb +126 -0
- data/spec/integration/obfuscation_spec.rb +69 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/test_db_setup/migrations/1_add_table_1.rb +18 -0
- data/spec/test_db_setup/migrations/2_add_table_2.rb +19 -0
- data/spec/test_db_setup/migrations/3_add_truncation_table_1.rb +14 -0
- data/spec/test_db_setup/migrations/4_add_whitelisted_table_1.rb +14 -0
- data/spec/test_db_setup/migrations/5_add_table_without_any_user_defined_obfuscation_strategies.rb +18 -0
- data/spec/test_db_setup/migrations/6_add_table_without_any_obfuscatable_columns.rb +15 -0
- data/spec/test_db_setup/migrations/7_add_audit_truncation_table.rb +13 -0
- data/spec/test_db_setup/seeds/audit_truncation_table.yml +7 -0
- data/spec/test_db_setup/seeds/table_1.yml +13 -0
- data/spec/test_db_setup/seeds/table_2.yml +15 -0
- data/spec/test_db_setup/seeds/table_without_any_obfuscatable_columns.yml +7 -0
- data/spec/test_db_setup/seeds/table_without_any_user_defined_obfuscation_strategies.yml +13 -0
- data/spec/test_db_setup/seeds/truncation_table_1.yml +9 -0
- data/spec/test_db_setup/seeds/whitelisted_table_1.yml +9 -0
- metadata +159 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'db_obfuscation/database'
|
2
|
+
|
3
|
+
module DbObfuscation
|
4
|
+
module ObfuscationStrategy
|
5
|
+
def self.strategy(column_name)
|
6
|
+
case column_name.to_sym
|
7
|
+
when /ssn/
|
8
|
+
:ssn_strategy
|
9
|
+
when /email/
|
10
|
+
:email_strategy
|
11
|
+
when :first_name
|
12
|
+
:first_name_strategy
|
13
|
+
when :last_name
|
14
|
+
:last_name_strategy
|
15
|
+
when :name
|
16
|
+
:name_strategy
|
17
|
+
else
|
18
|
+
:default_strategy
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ffaker'
|
2
|
+
module DbObfuscation
|
3
|
+
module Obfuscator
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def obfuscate(strategy_name)
|
7
|
+
case strategy_name
|
8
|
+
when :address_strategy
|
9
|
+
address
|
10
|
+
when :date_strategy
|
11
|
+
rand(31..240)
|
12
|
+
when :default_strategy
|
13
|
+
FFaker::Lorem.word
|
14
|
+
when :driving_license_strategy
|
15
|
+
FFaker::Identification.drivers_license
|
16
|
+
when :email_strategy
|
17
|
+
FFaker::Internet.safe_email
|
18
|
+
when :first_name_strategy
|
19
|
+
FFaker::Name.first_name
|
20
|
+
when :gender_strategy
|
21
|
+
'Unknown'
|
22
|
+
when :last_name_strategy
|
23
|
+
FFaker::Name.last_name
|
24
|
+
when :medicaid_id_strategy
|
25
|
+
FFaker.numerify('############')
|
26
|
+
when :name_strategy
|
27
|
+
FFaker::Name.name
|
28
|
+
when :nil_strategy
|
29
|
+
nil
|
30
|
+
when :paragraph_strategy
|
31
|
+
FFaker::Lorem.paragraph
|
32
|
+
when :phone_number_strategy
|
33
|
+
FFaker::PhoneNumber.phone_number
|
34
|
+
when :school_strategy
|
35
|
+
FFaker::Education.school
|
36
|
+
when :sentence_strategy
|
37
|
+
FFaker::Lorem.sentence
|
38
|
+
when :ssn_strategy
|
39
|
+
FFaker::SSN.ssn
|
40
|
+
when :suffix_strategy
|
41
|
+
FFaker::Name.suffix
|
42
|
+
when :unique_name_strategy
|
43
|
+
full_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def address
|
49
|
+
<<-ADDRESS.gsub(/\s{2,}/,' ').strip
|
50
|
+
#{FFaker::Address.street_address}
|
51
|
+
#{FFaker::Address.city}
|
52
|
+
#{FFaker::AddressUS.state_abbr}
|
53
|
+
#{FFaker::AddressUS.zip_code}
|
54
|
+
ADDRESS
|
55
|
+
end
|
56
|
+
|
57
|
+
def full_name
|
58
|
+
<<-name.gsub(/\s{2,}/,' ').strip
|
59
|
+
#{FFaker::Name.first_name}
|
60
|
+
#{FFaker.letterify('??????????')}
|
61
|
+
#{FFaker::Name.last_name}
|
62
|
+
name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module DbObfuscation
|
2
|
+
module QueryBuilder
|
3
|
+
class << self
|
4
|
+
def multi_update_sql(table, columns, date_columns=[])
|
5
|
+
<<-SQL.gsub(/\s{2,}/,' ').strip
|
6
|
+
UPDATE "#{table}"
|
7
|
+
#{case_sql(columns, date_columns)}
|
8
|
+
#{where_sql(columns)}
|
9
|
+
SQL
|
10
|
+
end
|
11
|
+
|
12
|
+
def where_sql(columns)
|
13
|
+
column_name = columns.first.keys.last
|
14
|
+
values = columns.map { |column| "'#{column.values.last}'" }.join(',')
|
15
|
+
%Q{WHERE "#{column_name}" IN (#{values})}
|
16
|
+
end
|
17
|
+
|
18
|
+
def case_sql(columns, date_columns)
|
19
|
+
column_names = columns.first.keys
|
20
|
+
constraint_name = column_names.last
|
21
|
+
column_names -= [constraint_name]
|
22
|
+
column_names.each_with_object('SET ') do |name, sql_query|
|
23
|
+
sql_query << %Q("#{name}" = CASE)
|
24
|
+
columns.each_with_object(sql_query) do |column, query|
|
25
|
+
type = (date_columns.include?(name) ? :date : :string)
|
26
|
+
query << when_sql(name, column, constraint_name, type)
|
27
|
+
end
|
28
|
+
sql_query << %Q( ELSE "#{name}" END, )
|
29
|
+
end.chomp(", ")
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_sql(table, id, columns, date_columns=[])
|
33
|
+
"UPDATE #{table} #{column_sql(columns, date_columns)} WHERE id=#{id};"
|
34
|
+
end
|
35
|
+
|
36
|
+
def column_sql(columns, date_columns)
|
37
|
+
columns.each_with_object("SET ") do |column, query|
|
38
|
+
column_name, column_value = column.first, column.last
|
39
|
+
if date_columns.include?(column.first)
|
40
|
+
query << "#{column_name}=#{column_name} + INTERVAL '#{column_value} DAYS', "
|
41
|
+
else
|
42
|
+
query << "#{column_name}=#{column_value}, "
|
43
|
+
end
|
44
|
+
end.chomp(", ")
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def when_sql(column, table_row, constraint, type)
|
49
|
+
when_clause = <<-SQL.gsub(/\s{2,}/,' ').chomp
|
50
|
+
WHEN "#{constraint}" = '#{table_row[constraint]}'
|
51
|
+
AND "#{column}" IS NOT NULL
|
52
|
+
SQL
|
53
|
+
if type == :date
|
54
|
+
when_clause << %Q( THEN "#{column}" + INTERVAL '#{table_row[column]} DAYS')
|
55
|
+
else
|
56
|
+
when_clause << %Q( AND "#{column}" != '' THEN #{table_row[column]})
|
57
|
+
end
|
58
|
+
when_clause
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'db_obfuscation/database'
|
2
|
+
require 'db_obfuscation/filtering/truncation'
|
3
|
+
|
4
|
+
module DbObfuscation
|
5
|
+
class Truncation
|
6
|
+
|
7
|
+
|
8
|
+
def self.truncate
|
9
|
+
new.send(:truncate)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.tables
|
13
|
+
new.send(:tables)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def truncate
|
19
|
+
tables.each do |table|
|
20
|
+
begin
|
21
|
+
DbObfuscation.logging.info "Truncating #{table}"
|
22
|
+
DB[table].truncate
|
23
|
+
rescue => e
|
24
|
+
DbObfuscation.logging.error "#{table} encountered #{e.message}"
|
25
|
+
DbObfuscation.logging.error "#{e.backtrace}"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def tables
|
32
|
+
Filtering::Truncation.matches_patterns(DB.tables, patterns)
|
33
|
+
end
|
34
|
+
|
35
|
+
def patterns
|
36
|
+
Config.truncation_patterns
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'db_obfuscation/database'
|
2
|
+
|
3
|
+
module DbObfuscation
|
4
|
+
module Util
|
5
|
+
module Trigger
|
6
|
+
extend self
|
7
|
+
def tables
|
8
|
+
query = <<-sql
|
9
|
+
SELECT tables.relname as table_name
|
10
|
+
FROM pg_trigger triggers, pg_class tables
|
11
|
+
WHERE triggers.tgrelid = tables.oid
|
12
|
+
AND tables.relname !~ '^pg_'
|
13
|
+
AND triggers.tgname LIKE '#{prefix}%'
|
14
|
+
sql
|
15
|
+
DbObfuscation::DB[query].map(:table_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def drop(table)
|
19
|
+
table == :all ? drop_triggers : drop_trigger(table)
|
20
|
+
end
|
21
|
+
|
22
|
+
def disable(table)
|
23
|
+
table == :all ? disable_triggers : disable_trigger(table)
|
24
|
+
end
|
25
|
+
|
26
|
+
def enable(table)
|
27
|
+
table == :all ? enable_triggers : enable_trigger(table)
|
28
|
+
end
|
29
|
+
|
30
|
+
def exists?(table)
|
31
|
+
tables.include?(table.to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def disable_triggers
|
36
|
+
tables.each do |table|
|
37
|
+
disable_trigger(table)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def disable_trigger(table)
|
42
|
+
DbObfuscation::DB.run <<-sql
|
43
|
+
ALTER TABLE #{table}
|
44
|
+
DISABLE TRIGGER #{name(table)}
|
45
|
+
sql
|
46
|
+
end
|
47
|
+
|
48
|
+
def enable_triggers
|
49
|
+
tables.each do |table|
|
50
|
+
enable_trigger(table)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def enable_trigger(table)
|
55
|
+
DbObfuscation::DB.run <<-sql
|
56
|
+
ALTER TABLE #{table}
|
57
|
+
ENABLE TRIGGER #{name(table)}
|
58
|
+
sql
|
59
|
+
end
|
60
|
+
|
61
|
+
def drop_triggers
|
62
|
+
tables.each do |table|
|
63
|
+
drop_trigger(table)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def drop_trigger(table)
|
68
|
+
DbObfuscation::DB.run <<-sql
|
69
|
+
DROP TRIGGER #{name(table)}
|
70
|
+
ON #{table}
|
71
|
+
sql
|
72
|
+
end
|
73
|
+
|
74
|
+
def prefix
|
75
|
+
'audit_'
|
76
|
+
end
|
77
|
+
|
78
|
+
def name(table)
|
79
|
+
"#{prefix}#{table}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'db_obfuscation/environment'
|
2
|
+
require 'db_dump'
|
3
|
+
|
4
|
+
module DbObfuscation
|
5
|
+
module Cli
|
6
|
+
describe DbDump do
|
7
|
+
|
8
|
+
it 'dump a db into a dump file' do
|
9
|
+
filename = 'db.yml'
|
10
|
+
db_config = {
|
11
|
+
'username' => 'fake_username',
|
12
|
+
'password' => 'fake_password',
|
13
|
+
'database' => 'fake_database',
|
14
|
+
'host' => 'fake_host'
|
15
|
+
}
|
16
|
+
|
17
|
+
allow(YAML).to receive(:load_file).with(filename).
|
18
|
+
and_return(db_config)
|
19
|
+
|
20
|
+
dump_name = 'dumped_database'
|
21
|
+
|
22
|
+
dump_cmd = <<-dump_cmd.gsub(/\s{2,}/,' ').strip
|
23
|
+
PGPASSWORD=fake_password pg_dump -h fake_host -U fake_username -w
|
24
|
+
-Fc -f dumped_database fake_database
|
25
|
+
dump_cmd
|
26
|
+
|
27
|
+
expect(Kernel).to receive(:system).with(dump_cmd)
|
28
|
+
described_class.dump(filename, dump_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'migrator'
|
3
|
+
require 'db_obfuscation/database'
|
4
|
+
|
5
|
+
module DbObfuscation
|
6
|
+
module Cli
|
7
|
+
describe Migrator do
|
8
|
+
describe '.migrate' do
|
9
|
+
before do
|
10
|
+
system("dropdb -e obfuscation_test")
|
11
|
+
system("createdb -e obfuscation_test")
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'creates table in the test database' do
|
15
|
+
expect(DbObfuscation::DB.tables).to eq []
|
16
|
+
|
17
|
+
db_yml = File.expand_path('../../config/database.yml',
|
18
|
+
__FILE__)
|
19
|
+
migration_path = File.expand_path('../../test_db_setup/migrations',
|
20
|
+
__FILE__)
|
21
|
+
|
22
|
+
described_class.migrate(db_yml, migration_path)
|
23
|
+
|
24
|
+
expect(DbObfuscation::DB.tables).to include :table_1
|
25
|
+
expect(DbObfuscation::DB.tables).to include :table_2
|
26
|
+
expect(DbObfuscation::DB.tables).to include :truncation_table_1
|
27
|
+
expect(DbObfuscation::DB.tables).to include :whitelisted_table_1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.down_migrate' do
|
32
|
+
before do
|
33
|
+
system("dropdb -e obfuscation_test")
|
34
|
+
system("createdb -e obfuscation_test")
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'creates table in the test database' do
|
38
|
+
db_yml = File.expand_path('../../config/database.yml',
|
39
|
+
__FILE__)
|
40
|
+
migration_path = File.expand_path('../../test_db_setup/migrations',
|
41
|
+
__FILE__)
|
42
|
+
|
43
|
+
described_class.migrate(db_yml, migration_path)
|
44
|
+
expect(DbObfuscation::DB.tables).to include :table_1
|
45
|
+
expect(DbObfuscation::DB.tables).to include :table_2
|
46
|
+
expect(DbObfuscation::DB.tables).to include :truncation_table_1
|
47
|
+
expect(DbObfuscation::DB.tables).to include :whitelisted_table_1
|
48
|
+
|
49
|
+
described_class.down_migrate(db_yml, migration_path)
|
50
|
+
|
51
|
+
expect(DbObfuscation::DB.tables).to eq([:schema_info])
|
52
|
+
|
53
|
+
#rescue operation to keep the test db in a clean state
|
54
|
+
described_class.migrate(db_yml, migration_path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'seeder'
|
3
|
+
require 'db_obfuscation/database'
|
4
|
+
require 'migrator'
|
5
|
+
|
6
|
+
module DbObfuscation
|
7
|
+
module Cli
|
8
|
+
describe Seeder do
|
9
|
+
let(:db_yml) {
|
10
|
+
File.expand_path('../../config/database.yml', __FILE__)
|
11
|
+
}
|
12
|
+
let(:migrations_path) {
|
13
|
+
File.expand_path('../../test_db_setup/migrations', __FILE__)
|
14
|
+
}
|
15
|
+
let(:seeds_path) {
|
16
|
+
File.expand_path('../../test_db_setup/seeds', __FILE__)
|
17
|
+
}
|
18
|
+
|
19
|
+
describe '.seed' do
|
20
|
+
before do
|
21
|
+
Migrator.down_migrate(db_yml, migrations_path)
|
22
|
+
Migrator.migrate(db_yml, migrations_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'seeds the database with seed data' do
|
26
|
+
expect(DbObfuscation::DB[:table_1].all).to eq []
|
27
|
+
Seeder.seed(db_yml, seeds_path)
|
28
|
+
expect(DbObfuscation::DB[:table_1].all).to_not eq([])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
- whitelisted_table_1
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'db_obfuscation/batch_formulator'
|
3
|
+
|
4
|
+
module DbObfuscation
|
5
|
+
describe BatchFormulator do
|
6
|
+
|
7
|
+
describe '.batch_for' do
|
8
|
+
it 'returns a batch of obfuscated row values for a set of ids' do
|
9
|
+
config = {
|
10
|
+
field_1: :default_strategy,
|
11
|
+
field_2: :default_strategy,
|
12
|
+
date_field: :default_strategy
|
13
|
+
}
|
14
|
+
|
15
|
+
ids = [1,2]
|
16
|
+
|
17
|
+
expected_batch = [
|
18
|
+
{ field_1: "'obfuscated_value'",
|
19
|
+
field_2: "'obfuscated_value'",
|
20
|
+
date_field: "'obfuscated_value'",
|
21
|
+
id: 1 },
|
22
|
+
{ field_1: "'obfuscated_value'",
|
23
|
+
field_2: "'obfuscated_value'",
|
24
|
+
date_field: "'obfuscated_value'",
|
25
|
+
id: 2 }
|
26
|
+
]
|
27
|
+
|
28
|
+
allow(DbObfuscation::Obfuscator).to receive(:obfuscate)
|
29
|
+
.and_return('obfuscated_value')
|
30
|
+
obfuscated_batch = described_class.batch_for(config, ids)
|
31
|
+
|
32
|
+
expect(obfuscated_batch).to eq expected_batch
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|