attr_masker 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +91 -0
  3. data/.gitignore +5 -1
  4. data/.rubocop.yml +13 -1069
  5. data/CHANGELOG.adoc +31 -0
  6. data/Gemfile +5 -0
  7. data/README.adoc +81 -30
  8. data/Rakefile +0 -27
  9. data/attr_masker.gemspec +15 -10
  10. data/bin/console +14 -0
  11. data/bin/rake +29 -0
  12. data/bin/rspec +29 -0
  13. data/bin/rubocop +29 -0
  14. data/bin/setup +9 -0
  15. data/gemfiles/Rails-4.2.gemfile +2 -3
  16. data/gemfiles/Rails-5.0.gemfile +2 -3
  17. data/gemfiles/Rails-5.1.gemfile +2 -3
  18. data/gemfiles/Rails-5.2.gemfile +4 -0
  19. data/gemfiles/Rails-6.0.gemfile +3 -0
  20. data/gemfiles/Rails-6.1.gemfile +3 -0
  21. data/gemfiles/Rails-head.gemfile +1 -3
  22. data/gemfiles/common.gemfile +4 -0
  23. data/lib/attr_masker.rb +6 -210
  24. data/lib/attr_masker/attribute.rb +80 -0
  25. data/lib/attr_masker/error.rb +1 -0
  26. data/lib/attr_masker/maskers/replacing.rb +20 -3
  27. data/lib/attr_masker/maskers/simple.rb +20 -5
  28. data/lib/attr_masker/model.rb +143 -0
  29. data/lib/attr_masker/performer.rb +56 -17
  30. data/lib/attr_masker/version.rb +1 -16
  31. data/lib/tasks/db.rake +13 -4
  32. data/spec/dummy/app/models/non_persisted_model.rb +2 -0
  33. data/spec/dummy/config/attr_masker.rb +1 -0
  34. data/spec/dummy/config/mongoid.yml +33 -0
  35. data/spec/dummy/config/routes.rb +0 -1
  36. data/spec/dummy/db/schema.rb +1 -0
  37. data/spec/features/active_record_spec.rb +97 -0
  38. data/spec/features/mongoid_spec.rb +36 -0
  39. data/spec/features/shared_examples.rb +382 -0
  40. data/spec/spec_helper.rb +26 -3
  41. data/spec/support/00_control_constants.rb +2 -0
  42. data/spec/support/10_mongoid_env.rb +9 -0
  43. data/spec/support/20_combustion.rb +10 -0
  44. data/spec/support/db_cleaner.rb +13 -2
  45. data/spec/support/force_config_file_reload.rb +9 -0
  46. data/spec/support/rake.rb +1 -1
  47. data/spec/unit/attribute_spec.rb +210 -0
  48. data/spec/{maskers → unit/maskers}/replacing_spec.rb +0 -0
  49. data/spec/{maskers → unit/maskers}/simple_spec.rb +2 -2
  50. data/spec/unit/model_spec.rb +12 -0
  51. data/spec/unit/rake_task_spec.rb +30 -0
  52. metadata +139 -32
  53. data/.travis.yml +0 -32
  54. data/gemfiles/Rails-4.0.gemfile +0 -5
  55. data/gemfiles/Rails-4.1.gemfile +0 -5
  56. data/spec/features_spec.rb +0 -203
  57. data/spec/support/0_combustion.rb +0 -5
data/CHANGELOG.adoc ADDED
@@ -0,0 +1,31 @@
1
+ == Not released yet
2
+
3
+ * Drop support for Rails < 4.2
4
+ * Allow masking attributes which span on more than one column (or field),
5
+ supersede `column_name` option with `column_names`
6
+
7
+ == 0.2.1
8
+
9
+ * Bugfix: preload application to find all models
10
+ * Bugfix: require all dependencies correctly
11
+
12
+ == 0.2.0
13
+
14
+ * `AttrMasker::Maskers::Simple` is now a class
15
+ * Added support for Mongoid
16
+
17
+ == 0.1.1
18
+
19
+ * Mask records disregarding default scope
20
+ (https://github.com/riboseinc/attr_masker/pull/41[#41])
21
+ * Major refactoring (extracting `Attribute` and `Model` classes)
22
+ * Code style improvements (nearly all violations fixed)
23
+
24
+ == 0.1
25
+
26
+ * First useful version
27
+ * Rails 4 & 5 compatibility
28
+ * Callable objects as maskers
29
+ * Nice progress bar
30
+ * Built-in maskers: `AttrMasker::Maskers::Replacing`
31
+ and `AttrMasker::Maskers::SIMPLE`
data/Gemfile CHANGED
@@ -1,3 +1,8 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
+
5
+ gem "mongoid", require: false
6
+
7
+ gem "database_cleaner-active_record", "~> 1.8", require: false
8
+ gem "database_cleaner-mongoid", "~> 1.8", require: false
data/README.adoc CHANGED
@@ -3,17 +3,33 @@
3
3
  :pygments-style: native
4
4
  :pygments-linenums-mode: inline
5
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!
6
+ ifdef::env-github[]
7
+ image:https://img.shields.io/gem/v/attr_masker[
8
+ "Gem Version",
9
+ link="https://rubygems.org/gems/attr_masker"]
10
+ image:https://img.shields.io/github/workflow/status/riboseinc/attr_masker/Tests[
11
+ "Build Status",
12
+ link="https://github.com/riboseinc/attr_masker/actions"]
13
+ image:https://img.shields.io/codeclimate/maintainability/riboseinc/attr_masker[
14
+ "Code Climate",
15
+ link="https://codeclimate.com/github/riboseinc/attr_masker"]
16
+ image:https://img.shields.io/codecov/c/github/riboseinc/attr_masker[
17
+ "Test Coverage",
18
+ link="https://codecov.io/gh/riboseinc/attr_masker"]
19
+ image:https://img.shields.io/badge/documentation-rdoc-informational[
20
+ "Documentation on RubyDoc.info",
21
+ link="https://rubydoc.info/gems/attr_masker"]
22
+ endif::[]
23
+
24
+ Mask ActiveRecord/Mongoid data with ease!
9
25
 
10
26
  == Introduction
11
27
 
12
28
  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.
29
+ can be used in staging or test environments. It works with Rails 4.2+ and
30
+ modern Rubies. It supports Active Record and Mongoid models.
15
31
 
16
- == Getting started
32
+ == Usage instructions
17
33
 
18
34
  === Installation
19
35
 
@@ -68,33 +84,16 @@ attr_masker :email :unless => ->(record) { ! record.tester_user? }
68
84
  attr_masker :first_name, :if => :tester_user?
69
85
  ----
70
86
 
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.
87
+ The ActiveRecord's `::default_scope` method has no effect on masking. All
88
+ table records are updated, provided that :if and :unless filters allow that.
89
+ For example, if you're using a https://github.com/rubysherpas/paranoia[Paranoia]
90
+ gem to soft-delete your data, records marked as deleted will be masked as well.
92
91
 
93
92
  === Built-in maskers
94
93
 
95
94
  Attr Masker comes with several built-in maskers.
96
95
 
97
- `AttrMasker::Maskers::SIMPLE`::
96
+ `AttrMasker::Maskers::Simple`::
98
97
  +
99
98
  Simply replaces any value with the `"(redacted)"`. Only useful for columns
100
99
  containing textual data.
@@ -106,7 +105,7 @@ Example:
106
105
  [source,ruby]
107
106
  ----
108
107
  attr_masker :first_name
109
- attr_masker :last_name, :masker => AttrMasker::Maskers::SIMPLE
108
+ attr_masker :last_name, :masker => AttrMasker::Maskers::Simple.new
110
109
  ----
111
110
  +
112
111
  Would set both `first_name` and `last_name` attributes to `"(redacted)"`.
@@ -120,7 +119,7 @@ Can be initialized with options.
120
119
  |===============================================================================
121
120
  |Name|Default|Description
122
121
  |`replacement`|`"*"`|Replacement string, can be empty.
123
- |`alphanum_only`|`false`|When true, only alphanumeric charaters are replaced.
122
+ |`alphanum_only`|`false`|When true, only alphanumeric characters are replaced.
124
123
  |===============================================================================
125
124
  +
126
125
  Example:
@@ -133,7 +132,59 @@ attr_masker :phone, :masker => rm
133
132
  +
134
133
  Would mask "123-456-7890" as "XXX-XXX-XXXX".
135
134
 
135
+ === Using custom maskers
136
+
137
+ Apart from built-in maskers, any object which responds to `#call` can be used,
138
+ e.g. some lambda or `Method` instance. For instance, you may want to produce
139
+ unique values basing on other attributes, to mask selectively, or to use
140
+ tool like https://github.com/skalee/well_read_faker[Well Read Faker] to
141
+ generate random replacement values:
142
+
143
+ [source,ruby]
144
+ ----
145
+ require "well_read_faker"
146
+
147
+ attr_masker :email, masker: ->(model:, **) { "user#{model.id}@example.com" }
148
+ attr_masker :phone, masker: ->(value:, **) { "******" + value[-3..-1] }
149
+ attr_masker :bio, masker: ->(**) { WellReadFaker.paragraph }
150
+ ----
151
+
152
+ Masker is called with following keyword arguments:
153
+
154
+ `value`:: Original value of the field which is about to be masked
155
+
156
+ `model`:: Model instance
157
+
158
+ `attribute_name`:: Name of the attribute which is about to be masked
159
+
160
+ `masking_options`:: Hash of options which were passed in `#attr_masker` call
161
+
162
+ This list is likely to be extended in future versions, and that will not be
163
+ considered a breaking change, therefore it is strongly recommended to always
164
+ use a splat (`**`) at end of argument list of masker's `#call` method.
165
+
166
+ === Configuration file
167
+
168
+ It is also possible to contain all the maskers configuration in one file.
169
+ Just place it in `config/attr_masker.rb`, and it will be loaded from a Rake
170
+ task after the application is fully loaded. That means you can re-open classes
171
+ and add masker definitions there, for example:
172
+
173
+ [source,ruby]
174
+ ----
175
+ # config/attr_masker.rb
176
+
177
+ class User
178
+ attr_masker :first_name, :last_name
179
+ end
180
+
181
+ class Email
182
+ attr_masker :address, ->(model:, **) { "mail#{model.id}@example.com" }
183
+ end
184
+ ----
185
+
136
186
  == Roadmap & TODOs
187
+
137
188
  - documentation
138
189
  - spec tests
139
190
  - Make the `Rails.env` (in which `db:mask` could be run) configurable
data/Rakefile CHANGED
@@ -1,32 +1,5 @@
1
1
  # (c) 2017 Ribose Inc.
2
2
  #
3
3
 
4
- # require 'rake'
5
- # require 'rake/testtask'
6
- # require 'rake/rdoctask'
7
-
8
4
  require "bundler/gem_tasks"
9
5
  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 CHANGED
@@ -1,13 +1,13 @@
1
1
  # (c) 2017 Ribose Inc.
2
2
  #
3
3
 
4
- lib = File.expand_path("../lib", __FILE__)
4
+ lib = File.expand_path("lib", __dir__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
  require "attr_masker/version"
7
7
 
8
8
  Gem::Specification.new do |gem|
9
9
  gem.name = "attr_masker"
10
- gem.version = AttrMasker::Version.string
10
+ gem.version = AttrMasker::VERSION
11
11
  gem.authors = ["Ribose Inc."]
12
12
  gem.email = ["open.source@ribose.com"]
13
13
  gem.homepage = "https://github.com/riboseinc/attr_masker"
@@ -17,18 +17,23 @@ Gem::Specification.new do |gem|
17
17
  "of certain models by modifying the database."
18
18
 
19
19
  gem.files = `git ls-files`.split($/)
20
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
20
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
21
  gem.require_paths = ["lib"]
23
22
 
24
- gem.add_runtime_dependency("rails", ">= 4.0.0", "< 6.0")
23
+ gem.add_runtime_dependency("rails", ">= 4.0.0", "< 7")
25
24
  gem.add_runtime_dependency("ruby-progressbar", "~> 1.8")
26
25
 
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")
26
+ gem.add_development_dependency("bundler", ">= 1.15")
27
+ gem.add_development_dependency("combustion", "~> 1.0")
28
+ gem.add_development_dependency("database_cleaner", "~> 1.8")
29
+ gem.add_development_dependency("database_cleaner-active_record", "~> 1.8")
30
+ gem.add_development_dependency("database_cleaner-mongoid", "~> 1.8")
31
+ # Older versions aren't needed as we don't support Rails < 4
32
+ gem.add_development_dependency("mongoid", ">= 5")
30
33
  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
+ gem.add_development_dependency("rspec", "~> 3.0")
35
+ gem.add_development_dependency("rubocop", "~> 0.54.0")
36
+ gem.add_development_dependency("simplecov")
37
+ gem.add_development_dependency("sqlite3", ">= 1.3.13", "< 2")
38
+ gem.add_development_dependency("warning", "~> 1.1")
34
39
  end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rack/cleanser"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # rubocop:disable all
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake") if File.exists?(Gem.bin_path("rake", "rake"))
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # rubocop:disable all
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # rubocop:disable all
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # rubocop:disable all
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ set -vx
6
+
7
+ bundle install
8
+
9
+ # Do any other automated setup that you need to do here
@@ -1,5 +1,4 @@
1
- source "https://rubygems.org"
1
+ eval_gemfile "common.gemfile"
2
2
 
3
3
  gem "activerecord", "~> 4.2.0"
4
-
5
- gemspec path: ".."
4
+ gem "sqlite3", "~> 1.3.13"
@@ -1,5 +1,4 @@
1
- source "https://rubygems.org"
1
+ eval_gemfile "common.gemfile"
2
2
 
3
3
  gem "activerecord", "~> 5.0.0"
4
-
5
- gemspec path: ".."
4
+ gem "sqlite3", "~> 1.3.13"
@@ -1,5 +1,4 @@
1
- source "https://rubygems.org"
1
+ eval_gemfile "common.gemfile"
2
2
 
3
3
  gem "activerecord", "~> 5.1.0"
4
-
5
- gemspec path: ".."
4
+ gem "sqlite3", "~> 1.3.13"
@@ -0,0 +1,4 @@
1
+ eval_gemfile "common.gemfile"
2
+
3
+ gem "activerecord", "~> 5.2.0"
4
+ gem "sqlite3", "~> 1.3.13"
@@ -0,0 +1,3 @@
1
+ eval_gemfile "common.gemfile"
2
+
3
+ gem "activerecord", "~> 6.0.0"
@@ -0,0 +1,3 @@
1
+ eval_gemfile "common.gemfile"
2
+
3
+ gem "activerecord", "~> 6.1.0"
@@ -1,6 +1,4 @@
1
- source "https://rubygems.org"
1
+ eval_gemfile "common.gemfile"
2
2
 
3
3
  gem "activerecord", github: "rails/rails"
4
4
  gem "arel", github: "rails/arel"
5
-
6
- gemspec path: ".."