attr_masker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +15 -0
- data/.gitignore +5 -0
- data/.hound.yml +3 -0
- data/.rspec +1 -0
- data/.rubocop.yml +1076 -0
- data/.travis.yml +32 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.adoc +153 -0
- data/Rakefile +32 -0
- data/attr_masker.gemspec +34 -0
- data/config.ru +7 -0
- data/gemfiles/Rails-4.0.gemfile +5 -0
- data/gemfiles/Rails-4.1.gemfile +5 -0
- data/gemfiles/Rails-4.2.gemfile +5 -0
- data/gemfiles/Rails-5.0.gemfile +5 -0
- data/gemfiles/Rails-5.1.gemfile +5 -0
- data/gemfiles/Rails-head.gemfile +6 -0
- data/lib/attr_masker/error.rb +6 -0
- data/lib/attr_masker/maskers/replacing.rb +30 -0
- data/lib/attr_masker/maskers/simple.rb +12 -0
- data/lib/attr_masker/performer.rb +67 -0
- data/lib/attr_masker/railtie.rb +17 -0
- data/lib/attr_masker/version.rb +21 -0
- data/lib/attr_masker.rb +227 -0
- data/lib/tasks/db.rake +21 -0
- data/spec/dummy/config/database.yml +3 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/db/schema.rb +8 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/features_spec.rb +203 -0
- data/spec/maskers/replacing_spec.rb +48 -0
- data/spec/maskers/simple_spec.rb +14 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/0_combustion.rb +5 -0
- data/spec/support/db_cleaner.rb +15 -0
- data/spec/support/matchers.rb +2 -0
- data/spec/support/rake.rb +6 -0
- data/spec/support/silence_stdout.rb +8 -0
- metadata +229 -0
data/.travis.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
sudo: false
|
2
|
+
dist: trusty
|
3
|
+
language: ruby
|
4
|
+
before_install: gem install bundler -v 1.15.1
|
5
|
+
|
6
|
+
script:
|
7
|
+
- bundle exec rspec
|
8
|
+
|
9
|
+
rvm:
|
10
|
+
- "2.4"
|
11
|
+
- "2.3"
|
12
|
+
- "ruby-head"
|
13
|
+
|
14
|
+
gemfile:
|
15
|
+
- gemfiles/Rails-5.1.gemfile
|
16
|
+
|
17
|
+
matrix:
|
18
|
+
include:
|
19
|
+
- rvm: "2.4"
|
20
|
+
gemfile: "gemfiles/Rails-head.gemfile"
|
21
|
+
- rvm: "2.4"
|
22
|
+
gemfile: "gemfiles/Rails-5.0.gemfile"
|
23
|
+
- rvm: "2.3"
|
24
|
+
gemfile: "gemfiles/Rails-4.2.gemfile"
|
25
|
+
- rvm: "2.3"
|
26
|
+
gemfile: "gemfiles/Rails-4.1.gemfile"
|
27
|
+
- rvm: "2.2"
|
28
|
+
gemfile: "gemfiles/Rails-4.0.gemfile"
|
29
|
+
|
30
|
+
allow_failures:
|
31
|
+
- rvm: "ruby-head"
|
32
|
+
- gemfile: "gemfiles/Rails-head.gemfile"
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Ribose
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.adoc
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
= attr_masker
|
2
|
+
:source-highlighter: pygments
|
3
|
+
:pygments-style: native
|
4
|
+
:pygments-linenums-mode: inline
|
5
|
+
|
6
|
+
image:https://img.shields.io/travis/riboseinc/attr_masker/master.svg["Build Status", link="https://travis-ci.org/riboseinc/attr_masker"]
|
7
|
+
|
8
|
+
Mask ActiveRecord data with ease!
|
9
|
+
|
10
|
+
== Introduction
|
11
|
+
|
12
|
+
This gem is intended to mask sensitive data so that production database dumps
|
13
|
+
can be used in staging or test environments. It works with Active Record 4+
|
14
|
+
and modern Rubies.
|
15
|
+
|
16
|
+
== Getting started
|
17
|
+
|
18
|
+
=== Installation
|
19
|
+
|
20
|
+
Add attr_masker to your gemfile:
|
21
|
+
|
22
|
+
[source,ruby]
|
23
|
+
----
|
24
|
+
gem "attr_masker", github: "riboseinc/attr_masker"
|
25
|
+
----
|
26
|
+
|
27
|
+
|
28
|
+
Then install the gem:
|
29
|
+
|
30
|
+
[source,sh]
|
31
|
+
----
|
32
|
+
bundle install
|
33
|
+
----
|
34
|
+
|
35
|
+
=== Basic usage
|
36
|
+
|
37
|
+
In your models, define attributes which should be masked:
|
38
|
+
|
39
|
+
[source,ruby]
|
40
|
+
----
|
41
|
+
class User
|
42
|
+
attr_masker :email, :first_name, :last_name
|
43
|
+
end
|
44
|
+
----
|
45
|
+
|
46
|
+
Then, when you want to mask the data, run the `db:mask` Rake task in some
|
47
|
+
Rails environment other than production, for example:
|
48
|
+
|
49
|
+
[source,sh]
|
50
|
+
----
|
51
|
+
bundle exec rake db:mask RAILS_ENV=staging
|
52
|
+
----
|
53
|
+
|
54
|
+
WARNING: Data are destructively overwritten. Run `rake db:mask` with care!
|
55
|
+
|
56
|
+
=== Masking records selectively
|
57
|
+
|
58
|
+
You can use `:if` and `:unless` options to prevent some records from being
|
59
|
+
altered.
|
60
|
+
|
61
|
+
[source,ruby]
|
62
|
+
----
|
63
|
+
# evaluates given proc for each record, and the record is passed as a proc's
|
64
|
+
# argument
|
65
|
+
attr_masker :email :unless => ->(record) { ! record.tester_user? }
|
66
|
+
|
67
|
+
# calls #tester_user? method on each record
|
68
|
+
attr_masker :first_name, :if => :tester_user?
|
69
|
+
----
|
70
|
+
|
71
|
+
=== Using custom maskers
|
72
|
+
|
73
|
+
By default, data is maksed with `AttrMasker::Maskers::SIMPLE` masker which
|
74
|
+
always returns `"(redacted)"` string. But anything what responds to `#call`
|
75
|
+
can be used instead: a lambda, `Method` instance, and more. You can specify it
|
76
|
+
by setting the `:masker` option.
|
77
|
+
|
78
|
+
For instance, you may want to use https://github.com/ffaker/ffaker[ffaker] or
|
79
|
+
https://github.com/skalee/well_read_faker[Well Read Faker] to generate random
|
80
|
+
replacement values:
|
81
|
+
|
82
|
+
[source,ruby]
|
83
|
+
----
|
84
|
+
require "ffaker"
|
85
|
+
|
86
|
+
attr_masker :first_name, :masker => ->(_hash) { FFaker::Name.first_name }
|
87
|
+
----
|
88
|
+
|
89
|
+
A hash is passed as an argument, which includes some information about the
|
90
|
+
record being masked and the attribute value. It can be used to further
|
91
|
+
customize masker's behaviour.
|
92
|
+
|
93
|
+
=== Built-in maskers
|
94
|
+
|
95
|
+
Attr Masker comes with several built-in maskers.
|
96
|
+
|
97
|
+
`AttrMasker::Maskers::SIMPLE`::
|
98
|
+
+
|
99
|
+
Simply replaces any value with the `"(redacted)"`. Only useful for columns
|
100
|
+
containing textual data.
|
101
|
+
+
|
102
|
+
This is a default masker. It is used when `:masker` option is unspecified.
|
103
|
+
+
|
104
|
+
Example:
|
105
|
+
+
|
106
|
+
[source,ruby]
|
107
|
+
----
|
108
|
+
attr_masker :first_name
|
109
|
+
attr_masker :last_name, :masker => AttrMasker::Maskers::SIMPLE
|
110
|
+
----
|
111
|
+
+
|
112
|
+
Would set both `first_name` and `last_name` attributes to `"(redacted)"`.
|
113
|
+
|
114
|
+
`AttrMasker::Maskers::Replacing`::
|
115
|
+
+
|
116
|
+
Replaces characters with some masker string (single asterisk by default).
|
117
|
+
Can be initialized with options.
|
118
|
+
+
|
119
|
+
[options="header"]
|
120
|
+
|===============================================================================
|
121
|
+
|Name|Default|Description
|
122
|
+
|`replacement`|`"*"`|Replacement string, can be empty.
|
123
|
+
|`alphanum_only`|`false`|When true, only alphanumeric charaters are replaced.
|
124
|
+
|===============================================================================
|
125
|
+
+
|
126
|
+
Example:
|
127
|
+
+
|
128
|
+
[source,ruby]
|
129
|
+
----
|
130
|
+
rm = AttrMasker::Maskers::Replacing.new(character: "X", alphanum_only: true)
|
131
|
+
attr_masker :phone, :masker => rm
|
132
|
+
----
|
133
|
+
+
|
134
|
+
Would mask "123-456-7890" as "XXX-XXX-XXXX".
|
135
|
+
|
136
|
+
== Roadmap & TODOs
|
137
|
+
- documentation
|
138
|
+
- spec tests
|
139
|
+
- Make the `Rails.env` (in which `db:mask` could be run) configurable
|
140
|
+
** maybe by passing `ENV` vars
|
141
|
+
- more masking options!
|
142
|
+
** default scrambling algorithms?
|
143
|
+
** structured text preserving algorithms
|
144
|
+
*** _e.g._, keeping an HTML snippet valid HTML, but with masked inner text
|
145
|
+
** structured *Object* preserving algorithms
|
146
|
+
*** _i.e._ generalization of the above HTML scenario
|
147
|
+
- I18n of the default `"(redacted)"` phrase
|
148
|
+
- …
|
149
|
+
|
150
|
+
== Acknowledgements
|
151
|
+
|
152
|
+
https://github.com/attr-encrypted/attr_encrypted[attr_encrypted] for the initial
|
153
|
+
code structure
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
# require 'rake'
|
5
|
+
# require 'rake/testtask'
|
6
|
+
# require 'rake/rdoctask'
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
load "tasks/db.rake"
|
10
|
+
|
11
|
+
# desc 'Generate documentation for the attr_masker gem.'
|
12
|
+
# Rake::RDocTask.new(:rdoc) do |rdoc|
|
13
|
+
# rdoc.rdoc_dir = 'rdoc'
|
14
|
+
# rdoc.title = 'attr_masker'
|
15
|
+
# rdoc.options << '--line-numbers' << '--inline-source'
|
16
|
+
# rdoc.rdoc_files.include('README*')
|
17
|
+
# rdoc.rdoc_files.include('lib/**/*.rb')
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# if RUBY_VERSION < '1.9.3'
|
21
|
+
# require 'rcov/rcovtask'
|
22
|
+
#
|
23
|
+
# task :rcov do
|
24
|
+
# system "rcov -o coverage/rcov --exclude '^(?!lib)' " + FileList[ 'test/**/*_test.rb' ].join(' ')
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# desc 'Default: run unit tests under rcov.'
|
28
|
+
# task :default => :rcov
|
29
|
+
# else
|
30
|
+
# desc 'Default: run unit tests.'
|
31
|
+
# task :default => :test
|
32
|
+
# end
|
data/attr_masker.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
lib = File.expand_path("../lib", __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require "attr_masker/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |gem|
|
9
|
+
gem.name = "attr_masker"
|
10
|
+
gem.version = AttrMasker::Version.string
|
11
|
+
gem.authors = ["Ribose Inc."]
|
12
|
+
gem.email = ["open.source@ribose.com"]
|
13
|
+
gem.homepage = "https://github.com/riboseinc/attr_masker"
|
14
|
+
gem.summary = "Masking attributes"
|
15
|
+
gem.licenses = ["MIT"]
|
16
|
+
gem.description = "It is desired to mask certain attributes " \
|
17
|
+
"of certain models by modifying the database."
|
18
|
+
|
19
|
+
gem.files = `git ls-files`.split($/)
|
20
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
|
24
|
+
gem.add_runtime_dependency("rails", ">= 4.0.0", "< 6.0")
|
25
|
+
gem.add_runtime_dependency("ruby-progressbar", "~> 1.8")
|
26
|
+
|
27
|
+
gem.add_development_dependency("bundler", "~> 1.15")
|
28
|
+
gem.add_development_dependency("combustion", "~> 0.7.0")
|
29
|
+
gem.add_development_dependency("database_cleaner", "~> 1.6")
|
30
|
+
gem.add_development_dependency("pry")
|
31
|
+
gem.add_development_dependency("rspec", ">= 3.0")
|
32
|
+
gem.add_development_dependency("rubocop", "~> 0.49.1")
|
33
|
+
gem.add_development_dependency("sqlite3", "~> 1.3.13")
|
34
|
+
end
|
data/config.ru
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
module AttrMasker
|
4
|
+
module Maskers
|
5
|
+
# This default masker simply replaces any value with a fixed string.
|
6
|
+
#
|
7
|
+
# +opts+ is a Hash with the key :value that gives you the current attribute
|
8
|
+
# value.
|
9
|
+
#
|
10
|
+
class Replacing
|
11
|
+
attr_reader :replacement, :alphanum_only
|
12
|
+
|
13
|
+
def initialize(replacement: "*", alphanum_only: false)
|
14
|
+
replacement = "" if replacement.nil?
|
15
|
+
@replacement = replacement
|
16
|
+
@alphanum_only = alphanum_only
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(value:, **_opts)
|
20
|
+
return value unless value.is_a? String
|
21
|
+
|
22
|
+
if alphanum_only
|
23
|
+
value.gsub(/[[:alnum:]]/, replacement)
|
24
|
+
else
|
25
|
+
replacement * value.size
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module AttrMasker
|
2
|
+
module Maskers
|
3
|
+
# This default masker simply replaces any value with a fixed string.
|
4
|
+
#
|
5
|
+
# +opts+ is a Hash with the key :value that gives you the current attribute
|
6
|
+
# value.
|
7
|
+
#
|
8
|
+
SIMPLE = lambda do |_opts|
|
9
|
+
"(redacted)"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
module AttrMasker
|
4
|
+
module Performer
|
5
|
+
class ActiveRecord
|
6
|
+
def mask
|
7
|
+
unless defined? ::ActiveRecord
|
8
|
+
raise AttrMasker::Error, "ActiveRecord undefined. Nothing to do!"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Do not want production environment to be masked!
|
12
|
+
#
|
13
|
+
if Rails.env.production?
|
14
|
+
raise AttrMasker::Error, "Attempted to run in production environment."
|
15
|
+
end
|
16
|
+
|
17
|
+
all_models.each do |klass|
|
18
|
+
next if klass.masker_attributes.empty?
|
19
|
+
mask_class(klass)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def mask_class(klass)
|
26
|
+
progressbar_for_model(klass) do |bar|
|
27
|
+
klass.all.each do |model|
|
28
|
+
mask_object model
|
29
|
+
bar.increment
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# For each masker attribute, mask it, and save it!
|
35
|
+
#
|
36
|
+
def mask_object(instance)
|
37
|
+
klass = instance.class
|
38
|
+
|
39
|
+
updates = klass.masker_attributes.reduce({}) do |acc, masker_attr|
|
40
|
+
attr_name = masker_attr[0]
|
41
|
+
column_name = masker_attr[1][:column_name] || attr_name
|
42
|
+
masker_value = instance.mask(attr_name)
|
43
|
+
acc.merge!(column_name => masker_value)
|
44
|
+
end
|
45
|
+
|
46
|
+
klass.all.update(instance.id, updates)
|
47
|
+
end
|
48
|
+
|
49
|
+
def progressbar_for_model(klass)
|
50
|
+
bar = ProgressBar.create(
|
51
|
+
title: klass.name,
|
52
|
+
total: klass.count,
|
53
|
+
throttle_rate: 0.1,
|
54
|
+
format: %q[%t %c/%C (%j%%) %B %E],
|
55
|
+
)
|
56
|
+
|
57
|
+
yield bar
|
58
|
+
ensure
|
59
|
+
bar.finish
|
60
|
+
end
|
61
|
+
|
62
|
+
def all_models
|
63
|
+
::ActiveRecord::Base.descendants.select(&:table_exists?)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
# URL:
|
4
|
+
# http://blog.nathanhumbert.com/2010/02/rails-3-loading-rake-tasks-from-gem.html
|
5
|
+
|
6
|
+
require "attr_masker"
|
7
|
+
require "rails"
|
8
|
+
|
9
|
+
module AttrMasker
|
10
|
+
class Railtie < Rails::Railtie
|
11
|
+
railtie_name :attr_masker
|
12
|
+
|
13
|
+
rake_tasks do
|
14
|
+
load "tasks/db.rake"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
module AttrMasker
|
5
|
+
# Contains information about this gem's version
|
6
|
+
module Version
|
7
|
+
MAJOR = 0
|
8
|
+
MINOR = 1
|
9
|
+
PATCH = 0
|
10
|
+
|
11
|
+
# Returns a version string by joining <tt>MAJOR</tt>, <tt>MINOR</tt>, and
|
12
|
+
# <tt>PATCH</tt> with <tt>'.'</tt>
|
13
|
+
#
|
14
|
+
# Example
|
15
|
+
#
|
16
|
+
# Version.string # '1.0.2'
|
17
|
+
def self.string
|
18
|
+
[MAJOR, MINOR, PATCH].join(".")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|