attr_masker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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