attr_masker 0.1.0 → 0.3.1
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 +5 -5
- data/.github/workflows/tests.yml +91 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +13 -1069
- data/CHANGELOG.adoc +31 -0
- data/Gemfile +5 -0
- data/README.adoc +81 -30
- data/Rakefile +0 -27
- data/attr_masker.gemspec +15 -10
- data/bin/console +14 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/setup +9 -0
- data/gemfiles/Rails-4.2.gemfile +2 -3
- data/gemfiles/Rails-5.0.gemfile +2 -3
- data/gemfiles/Rails-5.1.gemfile +2 -3
- data/gemfiles/Rails-5.2.gemfile +4 -0
- data/gemfiles/Rails-6.0.gemfile +3 -0
- data/gemfiles/Rails-6.1.gemfile +3 -0
- data/gemfiles/Rails-head.gemfile +1 -3
- data/gemfiles/common.gemfile +4 -0
- data/lib/attr_masker.rb +6 -210
- data/lib/attr_masker/attribute.rb +80 -0
- data/lib/attr_masker/error.rb +1 -0
- data/lib/attr_masker/maskers/replacing.rb +20 -3
- data/lib/attr_masker/maskers/simple.rb +20 -5
- data/lib/attr_masker/model.rb +143 -0
- data/lib/attr_masker/performer.rb +56 -17
- data/lib/attr_masker/version.rb +1 -16
- data/lib/tasks/db.rake +13 -4
- data/spec/dummy/app/models/non_persisted_model.rb +2 -0
- data/spec/dummy/config/attr_masker.rb +1 -0
- data/spec/dummy/config/mongoid.yml +33 -0
- data/spec/dummy/config/routes.rb +0 -1
- data/spec/dummy/db/schema.rb +1 -0
- data/spec/features/active_record_spec.rb +97 -0
- data/spec/features/mongoid_spec.rb +36 -0
- data/spec/features/shared_examples.rb +382 -0
- data/spec/spec_helper.rb +26 -3
- data/spec/support/00_control_constants.rb +2 -0
- data/spec/support/10_mongoid_env.rb +9 -0
- data/spec/support/20_combustion.rb +10 -0
- data/spec/support/db_cleaner.rb +13 -2
- data/spec/support/force_config_file_reload.rb +9 -0
- data/spec/support/rake.rb +1 -1
- data/spec/unit/attribute_spec.rb +210 -0
- data/spec/{maskers → unit/maskers}/replacing_spec.rb +0 -0
- data/spec/{maskers → unit/maskers}/simple_spec.rb +2 -2
- data/spec/unit/model_spec.rb +12 -0
- data/spec/unit/rake_task_spec.rb +30 -0
- metadata +139 -32
- data/.travis.yml +0 -32
- data/gemfiles/Rails-4.0.gemfile +0 -5
- data/gemfiles/Rails-4.1.gemfile +0 -5
- data/spec/features_spec.rb +0 -203
- data/spec/support/0_combustion.rb +0 -5
@@ -1,67 +1,106 @@
|
|
1
1
|
# (c) 2017 Ribose Inc.
|
2
2
|
#
|
3
|
+
|
3
4
|
module AttrMasker
|
4
5
|
module Performer
|
5
|
-
class
|
6
|
+
class Base
|
6
7
|
def mask
|
7
|
-
unless defined? ::ActiveRecord
|
8
|
-
raise AttrMasker::Error, "ActiveRecord undefined. Nothing to do!"
|
9
|
-
end
|
10
|
-
|
11
8
|
# Do not want production environment to be masked!
|
12
9
|
#
|
13
10
|
if Rails.env.production?
|
14
|
-
|
11
|
+
unless ENV["FORCE_MASK"]
|
12
|
+
msg = "Attempted to run in production environment."
|
13
|
+
raise AttrMasker::Error, msg
|
14
|
+
end
|
15
15
|
end
|
16
16
|
|
17
17
|
all_models.each do |klass|
|
18
18
|
next if klass.masker_attributes.empty?
|
19
|
+
|
19
20
|
mask_class(klass)
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
23
24
|
private
|
24
25
|
|
26
|
+
# Mask all objects of a class in batches to not run out of memory!
|
27
|
+
#--
|
28
|
+
# rubocop:todo Metrics/MethodLength
|
25
29
|
def mask_class(klass)
|
26
30
|
progressbar_for_model(klass) do |bar|
|
27
|
-
klass.all.
|
28
|
-
|
29
|
-
|
31
|
+
if klass.all.unscoped.respond_to?(:find_each)
|
32
|
+
klass.all.unscoped.find_each(batch_size: 1000) do |model|
|
33
|
+
mask_object model
|
34
|
+
bar.increment
|
35
|
+
end
|
36
|
+
else
|
37
|
+
klass.all.unscoped.each do |model|
|
38
|
+
mask_object model
|
39
|
+
bar.increment
|
40
|
+
end
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
44
|
+
# rubocop:enable Metrics/MethodLength
|
33
45
|
|
34
46
|
# For each masker attribute, mask it, and save it!
|
35
47
|
#
|
36
48
|
def mask_object(instance)
|
37
49
|
klass = instance.class
|
38
50
|
|
39
|
-
updates = klass.masker_attributes.reduce({}) do |acc,
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
acc.merge!(
|
51
|
+
updates = klass.masker_attributes.values.reduce({}) do |acc, attribute|
|
52
|
+
next acc unless attribute.should_mask?(instance)
|
53
|
+
|
54
|
+
attribute.mask(instance)
|
55
|
+
acc.merge! attribute.masked_attributes_new_values(instance)
|
44
56
|
end
|
45
57
|
|
46
|
-
|
58
|
+
make_update instance, updates unless updates.empty?
|
47
59
|
end
|
48
60
|
|
49
61
|
def progressbar_for_model(klass)
|
50
62
|
bar = ProgressBar.create(
|
51
63
|
title: klass.name,
|
52
|
-
total: klass.count,
|
64
|
+
total: klass.unscoped.count,
|
53
65
|
throttle_rate: 0.1,
|
54
|
-
format: %
|
66
|
+
format: "%t %c/%C (%j%%) %B %E",
|
55
67
|
)
|
56
68
|
|
57
69
|
yield bar
|
58
70
|
ensure
|
59
71
|
bar.finish
|
60
72
|
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class ActiveRecord < Base
|
76
|
+
def dependencies_available?
|
77
|
+
defined? ::ActiveRecord
|
78
|
+
end
|
61
79
|
|
62
80
|
def all_models
|
63
81
|
::ActiveRecord::Base.descendants.select(&:table_exists?)
|
64
82
|
end
|
83
|
+
|
84
|
+
#--
|
85
|
+
# rubocop:disable Rails/SkipsModelValidations
|
86
|
+
def make_update(instance, updates)
|
87
|
+
instance.class.all.unscoped.where(id: instance.id).update_all(updates)
|
88
|
+
end
|
89
|
+
# rubocop:enable Rails/SkipsModelValidations
|
90
|
+
end
|
91
|
+
|
92
|
+
class Mongoid < Base
|
93
|
+
def dependencies_available?
|
94
|
+
defined? ::Mongoid
|
95
|
+
end
|
96
|
+
|
97
|
+
def all_models
|
98
|
+
::Mongoid.models
|
99
|
+
end
|
100
|
+
|
101
|
+
def make_update(instance, updates)
|
102
|
+
instance.class.all.unscoped.where(id: instance.id).update(updates)
|
103
|
+
end
|
65
104
|
end
|
66
105
|
end
|
67
106
|
end
|
data/lib/attr_masker/version.rb
CHANGED
@@ -2,20 +2,5 @@
|
|
2
2
|
#
|
3
3
|
|
4
4
|
module AttrMasker
|
5
|
-
|
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
|
5
|
+
VERSION = "0.3.1".freeze
|
21
6
|
end
|
data/lib/tasks/db.rake
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
# (c) 2017 Ribose Inc.
|
2
2
|
#
|
3
3
|
|
4
|
-
# Hashrocket style looks better when describing task dependencies.
|
5
|
-
# rubocop:disable Style/HashSyntax
|
6
|
-
|
7
4
|
namespace :db do
|
8
5
|
desc "Mask every DB record according to rules set up in the respective " \
|
9
6
|
"ActiveRecord"
|
@@ -16,6 +13,18 @@ namespace :db do
|
|
16
13
|
# http://stackoverflow.com/questions/14163938/activerecordconnectionnotestablished-within-a-rake-task
|
17
14
|
#
|
18
15
|
task :mask => :environment do
|
19
|
-
|
16
|
+
Rails.application.eager_load!
|
17
|
+
|
18
|
+
config_file = Rails.root.join("config", "attr_masker.rb").to_s
|
19
|
+
require config_file if File.file?(config_file)
|
20
|
+
|
21
|
+
performers = AttrMasker::Performer::Base.descendants.map(&:new)
|
22
|
+
performers.select!(&:dependencies_available?)
|
23
|
+
|
24
|
+
if performers.empty?
|
25
|
+
raise AttrMasker::Error, "No supported database!"
|
26
|
+
end
|
27
|
+
|
28
|
+
performers.each(&:mask)
|
20
29
|
end
|
21
30
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
$CONFIG_LOADED_AT = Time.now
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Heavily based on Mongoid's test suite
|
2
|
+
# https://github.com/mongodb/mongoid/blob/v6.2.0/spec/config/mongoid.yml
|
3
|
+
test:
|
4
|
+
clients:
|
5
|
+
default:
|
6
|
+
database: attr_masker_test
|
7
|
+
hosts:
|
8
|
+
- <%=ENV["MONGOID_SPEC_HOST"]%>:<%=ENV["MONGOID_SPEC_PORT"]%>
|
9
|
+
options:
|
10
|
+
auth_source: "admin"
|
11
|
+
read:
|
12
|
+
mode: :primary_preferred
|
13
|
+
tag_sets:
|
14
|
+
- use: web
|
15
|
+
max_pool_size: 1
|
16
|
+
reports:
|
17
|
+
database: reports
|
18
|
+
hosts:
|
19
|
+
- <%=ENV["MONGOID_SPEC_HOST"]%>:<%=ENV["MONGOID_SPEC_PORT"]%>
|
20
|
+
options:
|
21
|
+
user: "mongoid-user"
|
22
|
+
password: "password"
|
23
|
+
auth_source: "admin"
|
24
|
+
options:
|
25
|
+
include_root_in_json: false
|
26
|
+
include_type_for_serialization: false
|
27
|
+
preload_models: false
|
28
|
+
scope_overwrite_exception: false
|
29
|
+
raise_not_found_error: true
|
30
|
+
use_activesupport_time_zone: true
|
31
|
+
use_utc: false
|
32
|
+
log_level: :warn
|
33
|
+
app_name: 'testing'
|
data/spec/dummy/config/routes.rb
CHANGED
data/spec/dummy/db/schema.rb
CHANGED
@@ -0,0 +1,97 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require_relative "shared_examples"
|
5
|
+
|
6
|
+
RSpec.describe "Attr Masker gem", :suppress_progressbar do
|
7
|
+
context "when used with ActiveRecord" do
|
8
|
+
before do
|
9
|
+
if WITHOUT_ACTIVE_RECORD
|
10
|
+
expect(defined?(::ActiveRecord)).to be(nil)
|
11
|
+
skip "Active Record specs disabled with WITHOUT=activerecord shell " \
|
12
|
+
"variable"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# For performance reasons, Rails features its own DescendantsTracker,
|
17
|
+
# which stores parent-child class relationships, and uses it to find
|
18
|
+
# subclasses instead of crawling the ObjectSpace.
|
19
|
+
#
|
20
|
+
# The drawback is that anonymous classes are never garbage collected,
|
21
|
+
# because there is always at least one reference, which is held by that
|
22
|
+
# tracker. Therefore, the return value of ActiveRecord::Base.descendants
|
23
|
+
# method call is typically polluted by anonymous classes created by this
|
24
|
+
# test suite. What is worse, that means that tests depend on each other.
|
25
|
+
#
|
26
|
+
# Till now, we used to stub ActiveRecord::Base.descendants method in specs
|
27
|
+
# where it matters, but that did not stop anonymous classes from being
|
28
|
+
# carried over to other examples, making the whole test suite quite
|
29
|
+
# fragile. And indeed, issues have been observed with Rails 5.2.
|
30
|
+
#
|
31
|
+
# This commit introduces a very different approach. Anonymous classes are
|
32
|
+
# removed from DescendantsTracker when respective test example is done.
|
33
|
+
# Nothing is carried over, no stubbing is necessary. It must be noted
|
34
|
+
# though that the new approach relies on Rails private APIs. But
|
35
|
+
# fortunately, DescendantsTracker is modified extremely rarely (no changes
|
36
|
+
# to AST since 2012), therefore I expect that this new approach will
|
37
|
+
# require much less maintenance than stubbing we did.
|
38
|
+
#
|
39
|
+
# -----
|
40
|
+
# 2020-01
|
41
|
+
#
|
42
|
+
# From Rails 6.0, DescendantsTracker uses weak references, and no longer
|
43
|
+
# blocks garbage collection of anonymous classes. See:
|
44
|
+
# https://github.com/rails/rails/pull/31442
|
45
|
+
#
|
46
|
+
# However, instances of these classes, which are bound to example life cycle
|
47
|
+
# via #let helpers, also hold the references, hence garbage collection must
|
48
|
+
# be postponed till example life cycle ends.
|
49
|
+
#
|
50
|
+
# Consequently, #after hooks cannot be used, as they are run too early for
|
51
|
+
# this purpose, but fortunately this can be worked around by
|
52
|
+
# before(:example) + after(:all) combo.
|
53
|
+
#
|
54
|
+
# -----
|
55
|
+
# 2020-01 part 2
|
56
|
+
#
|
57
|
+
# Not really. Garbage Collector cannot be trusted. It is not guaranteed
|
58
|
+
# to work consistently in different Ruby versions, and in fact is a cause
|
59
|
+
# of build failures in case of Rails 6.0 on Ruby 2.5.7 when run with
|
60
|
+
# WITHOUT=mongoid environment variable set.
|
61
|
+
#
|
62
|
+
# Therefore, manual sweep is still necessary in Ruby 6+.
|
63
|
+
after do
|
64
|
+
if defined?(::ActiveRecord::Base)
|
65
|
+
if ::ActiveSupport.gem_version < Gem::Version.new("6.0.0")
|
66
|
+
::ActiveSupport::DescendantsTracker.
|
67
|
+
class_variable_get("@@direct_descendants")[::ActiveRecord::Base].
|
68
|
+
delete(user_class_definition)
|
69
|
+
else
|
70
|
+
::ActiveSupport::DescendantsTracker.
|
71
|
+
class_variable_get("@@direct_descendants")[::ActiveRecord::Base].
|
72
|
+
instance_variable_get("@refs").
|
73
|
+
delete(user_class_definition)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Rails 5.2 seems to reset connection shortly after Combustion gets its job,
|
79
|
+
# causing in-memory database to be dropped. Hence, schema is loaded
|
80
|
+
# once again here.
|
81
|
+
before(:all) do
|
82
|
+
if defined?(::ActiveRecord::Base)
|
83
|
+
schema_path = File.expand_path("../dummy/db/schema.rb", __dir__)
|
84
|
+
load(schema_path)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# No point in using ApplicationRecord here.
|
89
|
+
# rubocop:disable Rails/ApplicationRecord
|
90
|
+
|
91
|
+
let(:user_class_definition) { Class.new(ActiveRecord::Base) }
|
92
|
+
|
93
|
+
# rubocop:enable Rails/ApplicationRecord
|
94
|
+
|
95
|
+
include_examples "Attr Masker gem feature specs"
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require_relative "shared_examples"
|
5
|
+
|
6
|
+
RSpec.describe "Attr Masker gem", :suppress_progressbar do
|
7
|
+
context "when used with Mongoid" do
|
8
|
+
before do
|
9
|
+
if WITHOUT_MONGOID
|
10
|
+
expect(defined?(::Mongoid)).to be(nil)
|
11
|
+
skip "Mongoid specs disabled with WITHOUT=mongoid shell variable"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
# Remove the example-specific model from Mongoid.models
|
17
|
+
::Mongoid.models.delete(user_class_definition) if defined?(::Mongoid)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:user_class_definition) do
|
21
|
+
Class.new do
|
22
|
+
include Mongoid::Document
|
23
|
+
include Mongoid::Timestamps
|
24
|
+
|
25
|
+
store_in collection: "users"
|
26
|
+
|
27
|
+
field :first_name
|
28
|
+
field :last_name
|
29
|
+
field :email
|
30
|
+
field :avatar
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
include_examples "Attr Masker gem feature specs"
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
# No point in ensuring a trailing comma in multiline argument lists here.
|
5
|
+
# rubocop:disable Style/TrailingCommaInArguments
|
6
|
+
|
7
|
+
require "spec_helper"
|
8
|
+
|
9
|
+
RSpec.shared_examples "Attr Masker gem feature specs" do
|
10
|
+
before do
|
11
|
+
stub_const "User", user_class_definition
|
12
|
+
|
13
|
+
User.class_eval do
|
14
|
+
def jedi?
|
15
|
+
email.ends_with? "@jedi.example.test"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let!(:han) do
|
21
|
+
User.create!(
|
22
|
+
first_name: "Han",
|
23
|
+
last_name: "Solo",
|
24
|
+
email: "han@example.test",
|
25
|
+
avatar: Marshal.dump("Millenium Falcon photo"),
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
let!(:luke) do
|
30
|
+
User.create!(
|
31
|
+
first_name: "Luke",
|
32
|
+
last_name: "Skywalker",
|
33
|
+
email: "luke@jedi.example.test",
|
34
|
+
avatar: Marshal.dump("photo with a light saber"),
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
example "Masking a single text attribute with default options" do
|
39
|
+
User.class_eval do
|
40
|
+
attr_masker :last_name
|
41
|
+
end
|
42
|
+
|
43
|
+
expect { run_rake_task }.not_to(change { User.count })
|
44
|
+
|
45
|
+
[han, luke].each do |record|
|
46
|
+
expect { record.reload }.to(
|
47
|
+
change { record.last_name }.to("(redacted)") &
|
48
|
+
preserve { record.first_name } &
|
49
|
+
preserve { record.email }
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
example "Specifying multiple attributes in an attr_masker declaration" do
|
55
|
+
User.class_eval do
|
56
|
+
attr_masker :first_name, :last_name
|
57
|
+
end
|
58
|
+
|
59
|
+
expect { run_rake_task }.not_to(change { User.count })
|
60
|
+
|
61
|
+
[han, luke].each do |record|
|
62
|
+
expect { record.reload }.to(
|
63
|
+
change { record.first_name }.to("(redacted)") &
|
64
|
+
change { record.last_name }.to("(redacted)") &
|
65
|
+
preserve { record.email }
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
example "Skipping some records when a symbol is passed to :if option" do
|
71
|
+
User.class_eval do
|
72
|
+
attr_masker :first_name, :last_name, if: :jedi?
|
73
|
+
end
|
74
|
+
|
75
|
+
expect { run_rake_task }.not_to(change { User.count })
|
76
|
+
|
77
|
+
expect { han.reload }.to(
|
78
|
+
preserve { han.first_name } &
|
79
|
+
preserve { han.last_name } &
|
80
|
+
preserve { han.email }
|
81
|
+
)
|
82
|
+
|
83
|
+
expect { luke.reload }.to(
|
84
|
+
change { luke.first_name }.to("(redacted)") &
|
85
|
+
change { luke.last_name }.to("(redacted)") &
|
86
|
+
preserve { luke.email }
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
example "Skipping some records when a lambda is passed to :if option" do
|
91
|
+
User.class_eval do
|
92
|
+
attr_masker :first_name, :last_name, if: ->(r) { r.jedi? }
|
93
|
+
end
|
94
|
+
|
95
|
+
expect { run_rake_task }.not_to(change { User.count })
|
96
|
+
|
97
|
+
expect { han.reload }.to(
|
98
|
+
preserve { han.first_name } &
|
99
|
+
preserve { han.last_name } &
|
100
|
+
preserve { han.email }
|
101
|
+
)
|
102
|
+
|
103
|
+
expect { luke.reload }.to(
|
104
|
+
change { luke.first_name }.to("(redacted)") &
|
105
|
+
change { luke.last_name }.to("(redacted)") &
|
106
|
+
preserve { luke.email }
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
example "Skipping some records when a symbol is passed to :unless option" do
|
111
|
+
User.class_eval do
|
112
|
+
attr_masker :first_name, :last_name, unless: :jedi?
|
113
|
+
end
|
114
|
+
|
115
|
+
expect { run_rake_task }.not_to(change { User.count })
|
116
|
+
|
117
|
+
expect { han.reload }.to(
|
118
|
+
change { han.first_name }.to("(redacted)") &
|
119
|
+
change { han.last_name }.to("(redacted)") &
|
120
|
+
preserve { han.email }
|
121
|
+
)
|
122
|
+
|
123
|
+
expect { luke.reload }.to(
|
124
|
+
preserve { luke.first_name } &
|
125
|
+
preserve { luke.last_name } &
|
126
|
+
preserve { luke.email }
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
example "Skipping some records when a lambda is passed to :unless option" do
|
131
|
+
User.class_eval do
|
132
|
+
attr_masker :first_name, :last_name, unless: ->(r) { r.jedi? }
|
133
|
+
end
|
134
|
+
|
135
|
+
expect { run_rake_task }.not_to(change { User.count })
|
136
|
+
|
137
|
+
expect { han.reload }.to(
|
138
|
+
change { han.first_name }.to("(redacted)") &
|
139
|
+
change { han.last_name }.to("(redacted)") &
|
140
|
+
preserve { han.email }
|
141
|
+
)
|
142
|
+
|
143
|
+
expect { luke.reload }.to(
|
144
|
+
preserve { luke.first_name } &
|
145
|
+
preserve { luke.last_name } &
|
146
|
+
preserve { luke.email }
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
example "Using a custom masker" do
|
151
|
+
reverse_masker = ->(value:, **_) do
|
152
|
+
value.reverse
|
153
|
+
end
|
154
|
+
|
155
|
+
upcase_masker = ->(value:, **_) do
|
156
|
+
value.upcase
|
157
|
+
end
|
158
|
+
|
159
|
+
User.class_eval do
|
160
|
+
attr_masker :first_name, masker: reverse_masker
|
161
|
+
attr_masker :last_name, masker: upcase_masker
|
162
|
+
end
|
163
|
+
|
164
|
+
expect { run_rake_task }.not_to(change { User.count })
|
165
|
+
|
166
|
+
expect { han.reload }.to(
|
167
|
+
change { han.first_name }.to("naH") &
|
168
|
+
change { han.last_name }.to("SOLO") &
|
169
|
+
preserve { han.email }
|
170
|
+
)
|
171
|
+
|
172
|
+
expect { luke.reload }.to(
|
173
|
+
change { luke.first_name }.to("ekuL") &
|
174
|
+
change { luke.last_name }.to("SKYWALKER") &
|
175
|
+
preserve { luke.email }
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
example "Using a custom masker with model" do
|
180
|
+
first_name_masker = ->(value:, model:, **_) do
|
181
|
+
value.reverse + model.email
|
182
|
+
end
|
183
|
+
|
184
|
+
last_name_masker = ->(value:, model:, **_) do
|
185
|
+
model.email + value.upcase
|
186
|
+
end
|
187
|
+
|
188
|
+
User.class_eval do
|
189
|
+
attr_masker :first_name, masker: first_name_masker
|
190
|
+
attr_masker :last_name, masker: last_name_masker
|
191
|
+
end
|
192
|
+
|
193
|
+
expect { run_rake_task }.not_to(change { User.count })
|
194
|
+
|
195
|
+
expect { han.reload }.to(
|
196
|
+
change { han.first_name }.to("naHhan@example.test") &
|
197
|
+
change { han.last_name }.to("han@example.testSOLO") &
|
198
|
+
preserve { han.email }
|
199
|
+
)
|
200
|
+
|
201
|
+
expect { luke.reload }.to(
|
202
|
+
change { luke.first_name }.to("ekuLluke@jedi.example.test") &
|
203
|
+
change { luke.last_name }.to("luke@jedi.example.testSKYWALKER") &
|
204
|
+
preserve { luke.email }
|
205
|
+
)
|
206
|
+
end
|
207
|
+
|
208
|
+
example "Masked value is assigned via attribute writer" do
|
209
|
+
User.class_eval do
|
210
|
+
attr_masker :first_name, :last_name
|
211
|
+
|
212
|
+
def first_name=(value)
|
213
|
+
self[:first_name] = "#{value} with side effects"
|
214
|
+
end
|
215
|
+
|
216
|
+
def last_name=(value)
|
217
|
+
self[:last_name] = value
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
expect { run_rake_task }.not_to(change { User.count })
|
222
|
+
|
223
|
+
expect { han.reload }.to(
|
224
|
+
change { han.first_name }.to("(redacted) with side effects") &
|
225
|
+
change { han.last_name }.to("(redacted)") &
|
226
|
+
preserve { han.email }
|
227
|
+
)
|
228
|
+
|
229
|
+
expect { luke.reload }.to(
|
230
|
+
change { luke.first_name }.to("(redacted) with side effects") &
|
231
|
+
change { luke.last_name }.to("(redacted)") &
|
232
|
+
preserve { luke.email }
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
example "Masking a marshalled attribute" do
|
237
|
+
User.class_eval do
|
238
|
+
attr_masker :avatar, marshal: true
|
239
|
+
end
|
240
|
+
|
241
|
+
expect { run_rake_task }.not_to(change { User.count })
|
242
|
+
|
243
|
+
expect { han.reload }.to(
|
244
|
+
preserve { han.first_name } &
|
245
|
+
preserve { han.last_name } &
|
246
|
+
preserve { han.email } &
|
247
|
+
change { han.avatar }
|
248
|
+
)
|
249
|
+
|
250
|
+
expect(han.avatar).to eq(Marshal.dump("(redacted)"))
|
251
|
+
|
252
|
+
expect { luke.reload }.to(
|
253
|
+
preserve { luke.first_name } &
|
254
|
+
preserve { luke.last_name } &
|
255
|
+
preserve { luke.email } &
|
256
|
+
change { luke.avatar }
|
257
|
+
)
|
258
|
+
|
259
|
+
expect(luke.avatar).to eq(Marshal.dump("(redacted)"))
|
260
|
+
end
|
261
|
+
|
262
|
+
example "Masking a marshalled attribute with a custom marshaller" do
|
263
|
+
module CustomMarshal
|
264
|
+
module_function
|
265
|
+
|
266
|
+
def load_marshalled(*args)
|
267
|
+
Marshal.load(*args) # rubocop:disable Security/MarshalLoad
|
268
|
+
end
|
269
|
+
|
270
|
+
def dump_json(*args)
|
271
|
+
JSON.dump(json: args)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
User.class_eval do
|
276
|
+
attr_masker(
|
277
|
+
:avatar,
|
278
|
+
marshal: true,
|
279
|
+
marshaler: CustomMarshal,
|
280
|
+
load_method: :load_marshalled,
|
281
|
+
dump_method: :dump_json,
|
282
|
+
)
|
283
|
+
end
|
284
|
+
|
285
|
+
expect { run_rake_task }.not_to(change { User.count })
|
286
|
+
|
287
|
+
expect { han.reload }.to(
|
288
|
+
preserve { han.first_name } &
|
289
|
+
preserve { han.last_name } &
|
290
|
+
preserve { han.email } &
|
291
|
+
change { han.avatar }
|
292
|
+
)
|
293
|
+
|
294
|
+
expect(han.avatar).to eq({ json: ["(redacted)"] }.to_json)
|
295
|
+
|
296
|
+
expect { luke.reload }.to(
|
297
|
+
preserve { luke.first_name } &
|
298
|
+
preserve { luke.last_name } &
|
299
|
+
preserve { luke.email } &
|
300
|
+
change { luke.avatar }
|
301
|
+
)
|
302
|
+
|
303
|
+
expect(luke.avatar).to eq({ json: ["(redacted)"] }.to_json)
|
304
|
+
end
|
305
|
+
|
306
|
+
example "Masking an attribute which spans on more than one table column \
|
307
|
+
(or document field)" do
|
308
|
+
User.class_eval do
|
309
|
+
attr_masker :full_name,
|
310
|
+
column_names: %i[first_name last_name],
|
311
|
+
masker: ->(**_) { "(first) (last)" }
|
312
|
+
|
313
|
+
def full_name
|
314
|
+
[first_name, last_name].join(" ")
|
315
|
+
end
|
316
|
+
|
317
|
+
def full_name=(value)
|
318
|
+
self.first_name = value.split(" ")[-2]
|
319
|
+
self.last_name = value.split(" ")[-1]
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
expect { run_rake_task }.not_to(change { User.count })
|
324
|
+
|
325
|
+
expect { han.reload }.to(
|
326
|
+
change { han.first_name }.to("(first)") &
|
327
|
+
change { han.last_name }.to("(last)") &
|
328
|
+
preserve { han.email } &
|
329
|
+
preserve { han.avatar }
|
330
|
+
)
|
331
|
+
|
332
|
+
expect { luke.reload }.to(
|
333
|
+
change { luke.first_name }.to("(first)") &
|
334
|
+
change { luke.last_name }.to("(last)") &
|
335
|
+
preserve { luke.email } &
|
336
|
+
preserve { luke.avatar }
|
337
|
+
)
|
338
|
+
end
|
339
|
+
|
340
|
+
example "It is disabled in production environment" do
|
341
|
+
allow(Rails).to receive(:env) { "production".inquiry }
|
342
|
+
|
343
|
+
User.class_eval do
|
344
|
+
attr_masker :last_name
|
345
|
+
end
|
346
|
+
|
347
|
+
expect { run_rake_task }.to(
|
348
|
+
preserve { User.count } &
|
349
|
+
raise_exception(AttrMasker::Error)
|
350
|
+
)
|
351
|
+
|
352
|
+
[han, luke].each do |record|
|
353
|
+
expect { record.reload }.not_to(change { record })
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
example "It masks records disregarding default scope" do
|
358
|
+
User.class_eval do
|
359
|
+
attr_masker :last_name
|
360
|
+
|
361
|
+
default_scope -> { where(last_name: "Solo") }
|
362
|
+
end
|
363
|
+
|
364
|
+
expect { run_rake_task }.not_to(change { User.unscoped.count })
|
365
|
+
|
366
|
+
[han, luke].each do |record|
|
367
|
+
expect { record.reload }.to(
|
368
|
+
change { record.last_name }.to("(redacted)")
|
369
|
+
)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
example "It loads configuration file", :force_config_file_reload do
|
374
|
+
expect { run_rake_task }.to change { $CONFIG_LOADED_AT }
|
375
|
+
end
|
376
|
+
|
377
|
+
def run_rake_task
|
378
|
+
Rake::Task["db:mask"].execute
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# rubocop:enable Style/TrailingCommaInArguments
|