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.
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
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -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,7 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :all
7
+ run Combustion::Application
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 4.0.0"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 4.1.0"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 4.2.0"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.0.0"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.1.0"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", github: "rails/rails"
4
+ gem "arel", github: "rails/arel"
5
+
6
+ gemspec path: ".."
@@ -0,0 +1,6 @@
1
+ # (c) 2017 Ribose Inc.
2
+ #
3
+ module AttrMasker
4
+ class Error < ::StandardError
5
+ end
6
+ end
@@ -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