attr_masker 0.1.0
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/.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
|