attr_masker 0.1.0 → 0.1.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 +4 -4
- data/CHANGELOG.adoc +15 -0
- data/README.adoc +6 -0
- data/Rakefile +0 -27
- data/attr_masker.gemspec +1 -1
- data/lib/attr_masker.rb +3 -209
- data/lib/attr_masker/attribute.rb +70 -0
- data/lib/attr_masker/model.rb +137 -0
- data/lib/attr_masker/performer.rb +8 -7
- data/lib/attr_masker/version.rb +1 -1
- data/spec/dummy/db/schema.rb +1 -0
- data/spec/features_spec.rb +88 -0
- data/spec/unit/attribute_spec.rb +128 -0
- data/spec/{maskers → unit/maskers}/replacing_spec.rb +0 -0
- data/spec/{maskers → unit/maskers}/simple_spec.rb +0 -0
- data/spec/unit/model_spec.rb +12 -0
- metadata +13 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43cbbd6cf1150e2228d2b5fc1323bbdbae852510
|
4
|
+
data.tar.gz: 614372bc5a85ac8b23112062b437a8d468064160
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb815eda36c7a99d2cfebf18df5a1b901a5e597df340b6f2041bd9bdb6ee722b4fac5b21dde71457ad63794de5cc3e1aec2659c02d32c2aea6e0bce7dfff2b8c
|
7
|
+
data.tar.gz: 6e97866032f1d8c1323e11bb160f62d9798f001b2736b98be03907f9269f39ff462be45633ac3c59c04856621196fc8979556c1438cac3b6be5eff68f1f6c238
|
data/CHANGELOG.adoc
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
== 0.1.1
|
2
|
+
|
3
|
+
* Mask records disregarding default scope
|
4
|
+
(https://github.com/riboseinc/attr_masker/pull/41[#41])
|
5
|
+
* Major refactoring (extracting `Attribute` and `Model` classes)
|
6
|
+
* Code style improvements (nearly all violations fixed)
|
7
|
+
|
8
|
+
== 0.1
|
9
|
+
|
10
|
+
* First useful version
|
11
|
+
* Rails 4 & 5 compatibility
|
12
|
+
* Callable objects as maskers
|
13
|
+
* Nice progress bar
|
14
|
+
* Built-in maskers: `AttrMasker::Maskers::Replacing`
|
15
|
+
and `AttrMasker::Maskers::SIMPLE`
|
data/README.adoc
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
:pygments-style: native
|
4
4
|
:pygments-linenums-mode: inline
|
5
5
|
|
6
|
+
image:https://img.shields.io/gem/v/attr_masker.svg["Gem Version", link="https://rubygems.org/gems/attr_masker"]
|
6
7
|
image:https://img.shields.io/travis/riboseinc/attr_masker/master.svg["Build Status", link="https://travis-ci.org/riboseinc/attr_masker"]
|
7
8
|
|
8
9
|
Mask ActiveRecord data with ease!
|
@@ -68,6 +69,11 @@ attr_masker :email :unless => ->(record) { ! record.tester_user? }
|
|
68
69
|
attr_masker :first_name, :if => :tester_user?
|
69
70
|
----
|
70
71
|
|
72
|
+
The ActiveRecord's `::default_scope` method has no effect on masking. All
|
73
|
+
table records are updated, provided that :if and :unless filters allow that.
|
74
|
+
For example, if you're using a Paranoia[https://github.com/rubysherpas/paranoia]
|
75
|
+
gem to soft-delete your data, records marked as deleted will be masked as well.
|
76
|
+
|
71
77
|
=== Using custom maskers
|
72
78
|
|
73
79
|
By default, data is maksed with `AttrMasker::Maskers::SIMPLE` masker which
|
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
@@ -17,7 +17,7 @@ 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) }
|
20
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
21
21
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
22
22
|
gem.require_paths = ["lib"]
|
23
23
|
|
data/lib/attr_masker.rb
CHANGED
@@ -4,6 +4,8 @@
|
|
4
4
|
# Adds attr_accessors that mask an object's attributes
|
5
5
|
module AttrMasker
|
6
6
|
autoload :Version, "attr_masker/version"
|
7
|
+
autoload :Attribute, "attr_masker/attribute"
|
8
|
+
autoload :Model, "attr_masker/model"
|
7
9
|
|
8
10
|
autoload :Error, "attr_masker/error"
|
9
11
|
autoload :Performer, "attr_masker/performer"
|
@@ -14,214 +16,6 @@ module AttrMasker
|
|
14
16
|
end
|
15
17
|
|
16
18
|
require "attr_masker/railtie" if defined?(Rails)
|
17
|
-
def self.extended(base) # :nodoc:
|
18
|
-
base.class_eval do
|
19
|
-
|
20
|
-
# Only include the dangerous instance methods during the Rake task!
|
21
|
-
include InstanceMethods
|
22
|
-
attr_writer :attr_masker_options
|
23
|
-
@attr_masker_options, @masker_attributes = {}, {}
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# Generates attr_accessors that mask attributes transparently
|
28
|
-
#
|
29
|
-
# Options (any other options you specify are passed to the masker's mask
|
30
|
-
# methods)
|
31
|
-
#
|
32
|
-
# :marshal => If set to true, attributes will be marshaled as well as masker. This is useful if you're planning
|
33
|
-
# on masking something other than a string. Defaults to false unless you're using it with ActiveRecord
|
34
|
-
# or DataMapper.
|
35
|
-
#
|
36
|
-
# :marshaler => The object to use for marshaling. Defaults to Marshal.
|
37
|
-
#
|
38
|
-
# :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
|
39
|
-
#
|
40
|
-
# :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
|
41
|
-
#
|
42
|
-
# :masker => The object to use for masking. It must respond to +#mask+. Defaults to AttrMasker::Maskers::Simple.
|
43
|
-
#
|
44
|
-
# :if => Attributes are only masker if this option evaluates to true. If you pass a symbol representing an instance
|
45
|
-
# method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
|
46
|
-
# Defaults to true.
|
47
|
-
#
|
48
|
-
# :unless => Attributes are only masker if this option evaluates to false. If you pass a symbol representing an instance
|
49
|
-
# method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
|
50
|
-
# Defaults to false.
|
51
|
-
#
|
52
|
-
# You can specify your own default options
|
53
|
-
#
|
54
|
-
# class User
|
55
|
-
# # now all attributes will be encoded and marshaled by default
|
56
|
-
# attr_masker_options.merge!(:marshal => true, :some_other_option => true)
|
57
|
-
# attr_masker :configuration
|
58
|
-
# end
|
59
|
-
#
|
60
|
-
#
|
61
|
-
# Example
|
62
|
-
#
|
63
|
-
# class User
|
64
|
-
# attr_masker :email, :credit_card
|
65
|
-
# attr_masker :configuration, :marshal => true
|
66
|
-
# end
|
67
|
-
#
|
68
|
-
# @user = User.new
|
69
|
-
# @user.masker_email # nil
|
70
|
-
# @user.email? # false
|
71
|
-
# @user.email = 'test@example.com'
|
72
|
-
# @user.email? # true
|
73
|
-
# @user.masker_email # returns the masker version of 'test@example.com'
|
74
|
-
#
|
75
|
-
# @user.configuration = { :time_zone => 'UTC' }
|
76
|
-
# @user.masker_configuration # returns the masker version of configuration
|
77
|
-
#
|
78
|
-
# See README for more examples
|
79
|
-
def attr_masker(*attributes)
|
80
|
-
options = {
|
81
|
-
:if => true,
|
82
|
-
:unless => false,
|
83
|
-
:column_name => nil,
|
84
|
-
:marshal => false,
|
85
|
-
:marshaler => Marshal,
|
86
|
-
:dump_method => "dump",
|
87
|
-
:load_method => "load",
|
88
|
-
:masker => AttrMasker::Maskers::SIMPLE,
|
89
|
-
}.merge!(attr_masker_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {})
|
90
|
-
|
91
|
-
attributes.each do |attribute|
|
92
|
-
masker_attributes[attribute.to_sym] = options.merge(attribute: attribute.to_sym)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# Default options to use with calls to <tt>attr_masker</tt>
|
97
|
-
# XXX:Keep
|
98
|
-
#
|
99
|
-
# It will inherit existing options from its superclass
|
100
|
-
def attr_masker_options
|
101
|
-
@attr_masker_options ||= superclass.attr_masker_options.dup
|
102
|
-
end
|
103
|
-
|
104
|
-
# Checks if an attribute is configured with <tt>attr_masker</tt>
|
105
|
-
# XXX:Keep
|
106
|
-
#
|
107
|
-
# Example
|
108
|
-
#
|
109
|
-
# class User
|
110
|
-
# attr_accessor :name
|
111
|
-
# attr_masker :email
|
112
|
-
# end
|
113
|
-
#
|
114
|
-
# User.attr_masker?(:name) # false
|
115
|
-
# User.attr_masker?(:email) # true
|
116
|
-
def attr_masker?(attribute)
|
117
|
-
masker_attributes.has_key?(attribute.to_sym)
|
118
|
-
end
|
119
|
-
|
120
|
-
# masks a value for the attribute specified
|
121
|
-
# XXX:modify
|
122
|
-
#
|
123
|
-
# Example
|
124
|
-
#
|
125
|
-
# class User
|
126
|
-
# attr_masker :email
|
127
|
-
# end
|
128
|
-
#
|
129
|
-
# masker_email = User.mask(:email, 'test@example.com')
|
130
|
-
def mask(attribute, value, options = {})
|
131
|
-
options = masker_attributes[attribute.to_sym].merge(options)
|
132
|
-
# if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
|
133
|
-
if options[:if] && !options[:unless]
|
134
|
-
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
|
135
|
-
masker_value = options[:masker].call(options.merge!(value: value))
|
136
|
-
masker_value
|
137
|
-
else
|
138
|
-
value
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
# Contains a hash of masker attributes with virtual attribute names as keys
|
143
|
-
# and their corresponding options as values
|
144
|
-
# XXX:Keep
|
145
|
-
#
|
146
|
-
# Example
|
147
|
-
#
|
148
|
-
# class User
|
149
|
-
# attr_masker :email
|
150
|
-
# end
|
151
|
-
#
|
152
|
-
# User.masker_attributes # { :email => { :attribute => 'masker_email' } }
|
153
|
-
def masker_attributes
|
154
|
-
@masker_attributes ||= superclass.masker_attributes.dup
|
155
|
-
end
|
156
|
-
|
157
|
-
# Forwards calls to :mask_#{attribute} to the corresponding mask method
|
158
|
-
# if attribute was configured with attr_masker
|
159
|
-
#
|
160
|
-
# Example
|
161
|
-
#
|
162
|
-
# class User
|
163
|
-
# attr_masker :email
|
164
|
-
# end
|
165
|
-
#
|
166
|
-
# User.mask_email('SOME_masker_EMAIL_STRING')
|
167
|
-
def method_missing(method, *arguments, &block)
|
168
|
-
if method.to_s =~ /^mask_(.+)$/ && attr_masker?($1)
|
169
|
-
send(:mask, $1, *arguments)
|
170
|
-
else
|
171
|
-
super
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
module InstanceMethods
|
176
|
-
|
177
|
-
# masks a value for the attribute specified using options evaluated in the current object's scope
|
178
|
-
#
|
179
|
-
# Example
|
180
|
-
#
|
181
|
-
# class User
|
182
|
-
# attr_accessor :secret_key
|
183
|
-
# attr_masker :email
|
184
|
-
#
|
185
|
-
# def initialize(secret_key)
|
186
|
-
# self.secret_key = secret_key
|
187
|
-
# end
|
188
|
-
# end
|
189
|
-
#
|
190
|
-
# @user = User.new('some-secret-key')
|
191
|
-
# @user.mask(:email, 'test@example.com')
|
192
|
-
def mask(attribute, value=nil)
|
193
|
-
value = self.send(attribute) if value.nil?
|
194
|
-
self.class.mask(attribute, value, evaluated_attr_masker_options_for(attribute))
|
195
|
-
end
|
196
|
-
|
197
|
-
protected
|
198
|
-
|
199
|
-
# Returns attr_masker options evaluated in the current object's scope for the attribute specified
|
200
|
-
# XXX:Keep
|
201
|
-
def evaluated_attr_masker_options_for(attribute)
|
202
|
-
self.class.masker_attributes[attribute.to_sym].inject({}) do |hash, (option, value)|
|
203
|
-
if %i[if unless].include?(option)
|
204
|
-
hash.merge!(option => evaluate_attr_masker_option(value))
|
205
|
-
else
|
206
|
-
hash.merge!(option => value)
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# Evaluates symbol (method reference) or proc (responds to call) options
|
212
|
-
# XXX:Keep
|
213
|
-
#
|
214
|
-
# If the option is not a symbol or proc then the original option is returned
|
215
|
-
def evaluate_attr_masker_option(option)
|
216
|
-
if option.is_a?(Symbol) && respond_to?(option)
|
217
|
-
send(option)
|
218
|
-
elsif option.respond_to?(:call)
|
219
|
-
option.call(self)
|
220
|
-
else
|
221
|
-
option
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|
225
19
|
end
|
226
20
|
|
227
|
-
Object.extend AttrMasker
|
21
|
+
Object.extend AttrMasker::Model
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
module AttrMasker
|
5
|
+
# Holds the definition of maskable attribute.
|
6
|
+
class Attribute
|
7
|
+
attr_reader :name, :model, :options
|
8
|
+
|
9
|
+
def initialize(name, model, options)
|
10
|
+
@name = name.to_sym
|
11
|
+
@model = model
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Evaluates the +:if+ and +:unless+ attribute options on given instance.
|
16
|
+
# Returns +true+ or +fasle+, depending on whether the attribute should be
|
17
|
+
# masked for this object or not.
|
18
|
+
def should_mask?(model_instance)
|
19
|
+
not (
|
20
|
+
options.key?(:if) && !evaluate_option(:if, model_instance) ||
|
21
|
+
options.key?(:unless) && evaluate_option(:unless, model_instance)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Mask the attribute on given model. Masking will be performed regardless
|
26
|
+
# of +:if+ and +:unless+ options. A +should_mask?+ method should be called
|
27
|
+
# separately to ensure that given object is eligible for masking.
|
28
|
+
#
|
29
|
+
# The method returns the masked value but does not modify the object's
|
30
|
+
# attribute.
|
31
|
+
#
|
32
|
+
# If +marshal+ attribute's option is +true+, the attribute value will be
|
33
|
+
# loaded before masking, and dumped to proper storage format prior
|
34
|
+
# returning.
|
35
|
+
def mask(model_instance)
|
36
|
+
value = unmarshal_data(model_instance.send(name))
|
37
|
+
masker_value = options[:masker].call(options.merge!(value: value))
|
38
|
+
marshal_data(masker_value)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Evaluates option (typically +:if+ or +:unless+) on given model instance.
|
42
|
+
# That option can be either a proc (a model is passed as an only argument),
|
43
|
+
# or a symbol (a method of that name is called on model instance).
|
44
|
+
def evaluate_option(option_name, model_instance)
|
45
|
+
option = options[option_name]
|
46
|
+
|
47
|
+
if option.is_a?(Symbol)
|
48
|
+
model_instance.send(option)
|
49
|
+
elsif option.respond_to?(:call)
|
50
|
+
option.call(model_instance)
|
51
|
+
else
|
52
|
+
option
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def marshal_data(data)
|
57
|
+
return data unless options[:marshal]
|
58
|
+
options[:marshaler].send(options[:dump_method], data)
|
59
|
+
end
|
60
|
+
|
61
|
+
def unmarshal_data(data)
|
62
|
+
return data unless options[:marshal]
|
63
|
+
options[:marshaler].send(options[:load_method], data)
|
64
|
+
end
|
65
|
+
|
66
|
+
def column_name
|
67
|
+
options[:column_name] || name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module AttrMasker
|
2
|
+
module Model
|
3
|
+
def self.extended(base) # :nodoc:
|
4
|
+
base.class_eval do
|
5
|
+
attr_writer :attr_masker_options
|
6
|
+
@attr_masker_options = {}
|
7
|
+
@masker_attributes = {}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generates attr_accessors that mask attributes transparently
|
12
|
+
#
|
13
|
+
# Options (any other options you specify are passed to the masker's mask
|
14
|
+
# methods)
|
15
|
+
#
|
16
|
+
# [:masker]
|
17
|
+
# The object to use for masking. It must respond to +#mask+. Defaults to
|
18
|
+
# AttrMasker::Maskers::Simple.
|
19
|
+
#
|
20
|
+
# [:if]
|
21
|
+
# Attributes are only masker if this option evaluates to true. If you
|
22
|
+
# pass a symbol representing an instance method then the result of
|
23
|
+
# the method will be evaluated. Any objects that respond to
|
24
|
+
# <tt>:call</tt> are evaluated as well. Defaults to true.
|
25
|
+
#
|
26
|
+
# [:unless]
|
27
|
+
# Attributes are only masker if this option evaluates to false. If you
|
28
|
+
# pass a symbol representing an instance method then the result of
|
29
|
+
# the method will be evaluated. Any objects that respond to
|
30
|
+
# <tt>:call</tt> are evaluated as well. Defaults to false.
|
31
|
+
#
|
32
|
+
# [:marshal]
|
33
|
+
# If set to true, attributes will be marshaled as well as masker. This
|
34
|
+
# is useful if you're planning on masking something other than a string.
|
35
|
+
# Defaults to false unless you're using it with ActiveRecord or
|
36
|
+
# DataMapper.
|
37
|
+
#
|
38
|
+
# [:marshaler]
|
39
|
+
# The object to use for marshaling. Defaults to Marshal.
|
40
|
+
#
|
41
|
+
# [:dump_method]
|
42
|
+
# The dump method name to call on the <tt>:marshaler</tt> object to.
|
43
|
+
# Defaults to 'dump'.
|
44
|
+
#
|
45
|
+
# [:load_method]
|
46
|
+
# The load method name to call on the <tt>:marshaler</tt> object.
|
47
|
+
# Defaults to 'load'.
|
48
|
+
#
|
49
|
+
# You can specify your own default options
|
50
|
+
#
|
51
|
+
# class User
|
52
|
+
# # now all attributes will be encoded and marshaled by default
|
53
|
+
# attr_masker_options.merge!(:marshal => true, :another_option => true)
|
54
|
+
# attr_masker :configuration
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
#
|
58
|
+
# Example
|
59
|
+
#
|
60
|
+
# class User
|
61
|
+
# attr_masker :email, :credit_card
|
62
|
+
# attr_masker :configuration, :marshal => true
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# @user = User.new
|
66
|
+
# @user.masker_email # nil
|
67
|
+
# @user.email? # false
|
68
|
+
# @user.email = 'test@example.com'
|
69
|
+
# @user.email? # true
|
70
|
+
# @user.masker_email # returns the masker version of 'test@example.com'
|
71
|
+
#
|
72
|
+
# @user.configuration = { :time_zone => 'UTC' }
|
73
|
+
# @user.masker_configuration # returns the masker version of configuration
|
74
|
+
#
|
75
|
+
# See README for more examples
|
76
|
+
def attr_masker(*args)
|
77
|
+
default_options = {
|
78
|
+
if: true,
|
79
|
+
unless: false,
|
80
|
+
column_name: nil,
|
81
|
+
marshal: false,
|
82
|
+
marshaler: Marshal,
|
83
|
+
dump_method: "dump",
|
84
|
+
load_method: "load",
|
85
|
+
masker: AttrMasker::Maskers::SIMPLE,
|
86
|
+
}
|
87
|
+
|
88
|
+
options = args.extract_options!.
|
89
|
+
reverse_merge(attr_masker_options).
|
90
|
+
reverse_merge(default_options)
|
91
|
+
|
92
|
+
args.each do |attribute_name|
|
93
|
+
attribute = Attribute.new(attribute_name, self, options)
|
94
|
+
masker_attributes[attribute.name] = attribute
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Default options to use with calls to <tt>attr_masker</tt>
|
99
|
+
# XXX:Keep
|
100
|
+
#
|
101
|
+
# It will inherit existing options from its superclass
|
102
|
+
def attr_masker_options
|
103
|
+
@attr_masker_options ||= superclass.attr_masker_options.dup
|
104
|
+
end
|
105
|
+
|
106
|
+
# Checks if an attribute is configured with <tt>attr_masker</tt>
|
107
|
+
# XXX:Keep
|
108
|
+
#
|
109
|
+
# Example
|
110
|
+
#
|
111
|
+
# class User
|
112
|
+
# attr_accessor :name
|
113
|
+
# attr_masker :email
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# User.attr_masker?(:name) # false
|
117
|
+
# User.attr_masker?(:email) # true
|
118
|
+
def attr_masker?(attribute)
|
119
|
+
masker_attributes.has_key?(attribute.to_sym)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Contains a hash of masker attributes with virtual attribute names as keys
|
123
|
+
# and their corresponding options as values
|
124
|
+
# XXX:Keep
|
125
|
+
#
|
126
|
+
# Example
|
127
|
+
#
|
128
|
+
# class User
|
129
|
+
# attr_masker :email
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# User.masker_attributes # { :email => { :attribute => 'masker_email' } }
|
133
|
+
def masker_attributes
|
134
|
+
@masker_attributes ||= superclass.masker_attributes.dup
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -24,7 +24,7 @@ module AttrMasker
|
|
24
24
|
|
25
25
|
def mask_class(klass)
|
26
26
|
progressbar_for_model(klass) do |bar|
|
27
|
-
klass.all.each do |model|
|
27
|
+
klass.all.unscoped.each do |model|
|
28
28
|
mask_object model
|
29
29
|
bar.increment
|
30
30
|
end
|
@@ -36,20 +36,21 @@ module AttrMasker
|
|
36
36
|
def mask_object(instance)
|
37
37
|
klass = instance.class
|
38
38
|
|
39
|
-
updates = klass.masker_attributes.reduce({}) do |acc,
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
updates = klass.masker_attributes.values.reduce({}) do |acc, attribute|
|
40
|
+
next acc unless attribute.should_mask?(instance)
|
41
|
+
|
42
|
+
column_name = attribute.column_name
|
43
|
+
masker_value = attribute.mask(instance)
|
43
44
|
acc.merge!(column_name => masker_value)
|
44
45
|
end
|
45
46
|
|
46
|
-
klass.all.update(instance.id, updates)
|
47
|
+
klass.all.unscoped.update(instance.id, updates)
|
47
48
|
end
|
48
49
|
|
49
50
|
def progressbar_for_model(klass)
|
50
51
|
bar = ProgressBar.create(
|
51
52
|
title: klass.name,
|
52
|
-
total: klass.count,
|
53
|
+
total: klass.unscoped.count,
|
53
54
|
throttle_rate: 0.1,
|
54
55
|
format: %q[%t %c/%C (%j%%) %B %E],
|
55
56
|
)
|
data/lib/attr_masker/version.rb
CHANGED
data/spec/dummy/db/schema.rb
CHANGED
data/spec/features_spec.rb
CHANGED
@@ -28,6 +28,7 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
|
|
28
28
|
first_name: "Han",
|
29
29
|
last_name: "Solo",
|
30
30
|
email: "han@example.test",
|
31
|
+
avatar: Marshal.dump("Millenium Falcon photo"),
|
31
32
|
)
|
32
33
|
end
|
33
34
|
|
@@ -36,6 +37,7 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
|
|
36
37
|
first_name: "Luke",
|
37
38
|
last_name: "Skywalker",
|
38
39
|
email: "luke@jedi.example.test",
|
40
|
+
avatar: Marshal.dump("photo with a light saber"),
|
39
41
|
)
|
40
42
|
end
|
41
43
|
|
@@ -180,6 +182,76 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
|
|
180
182
|
)
|
181
183
|
end
|
182
184
|
|
185
|
+
example "Masking a marshalled attribute" do
|
186
|
+
User.class_eval do
|
187
|
+
attr_masker :avatar, marshal: true
|
188
|
+
end
|
189
|
+
|
190
|
+
expect { run_rake_task }.not_to(change { User.count })
|
191
|
+
|
192
|
+
expect { han.reload }.to(
|
193
|
+
preserve { han.first_name } &
|
194
|
+
preserve { han.last_name } &
|
195
|
+
preserve { han.email } &
|
196
|
+
change { han.avatar }
|
197
|
+
)
|
198
|
+
|
199
|
+
expect(han.avatar).to eq(Marshal.dump("(redacted)"))
|
200
|
+
|
201
|
+
expect { luke.reload }.to(
|
202
|
+
preserve { luke.first_name } &
|
203
|
+
preserve { luke.last_name } &
|
204
|
+
preserve { luke.email } &
|
205
|
+
change { luke.avatar }
|
206
|
+
)
|
207
|
+
|
208
|
+
expect(luke.avatar).to eq(Marshal.dump("(redacted)"))
|
209
|
+
end
|
210
|
+
|
211
|
+
example "Masking a marshalled attribute with a custom marshaller" do
|
212
|
+
module CustomMarshal
|
213
|
+
module_function
|
214
|
+
|
215
|
+
def load_marshalled(*args)
|
216
|
+
Marshal.load(*args) # rubocop:disable Security/MarshalLoad
|
217
|
+
end
|
218
|
+
|
219
|
+
def dump_json(*args)
|
220
|
+
JSON.dump(json: args)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
User.class_eval do
|
225
|
+
attr_masker(
|
226
|
+
:avatar,
|
227
|
+
marshal: true,
|
228
|
+
marshaler: CustomMarshal,
|
229
|
+
load_method: :load_marshalled,
|
230
|
+
dump_method: :dump_json,
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
expect { run_rake_task }.not_to(change { User.count })
|
235
|
+
|
236
|
+
expect { han.reload }.to(
|
237
|
+
preserve { han.first_name } &
|
238
|
+
preserve { han.last_name } &
|
239
|
+
preserve { han.email } &
|
240
|
+
change { han.avatar }
|
241
|
+
)
|
242
|
+
|
243
|
+
expect(han.avatar).to eq({ json: ["(redacted)"] }.to_json)
|
244
|
+
|
245
|
+
expect { luke.reload }.to(
|
246
|
+
preserve { luke.first_name } &
|
247
|
+
preserve { luke.last_name } &
|
248
|
+
preserve { luke.email } &
|
249
|
+
change { luke.avatar }
|
250
|
+
)
|
251
|
+
|
252
|
+
expect(luke.avatar).to eq({ json: ["(redacted)"] }.to_json)
|
253
|
+
end
|
254
|
+
|
183
255
|
example "It is disabled in production environment" do
|
184
256
|
allow(Rails).to receive(:env) { "production".inquiry }
|
185
257
|
|
@@ -197,6 +269,22 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
|
|
197
269
|
end
|
198
270
|
end
|
199
271
|
|
272
|
+
example "It masks records disregarding default scope" do
|
273
|
+
User.class_eval do
|
274
|
+
attr_masker :last_name
|
275
|
+
|
276
|
+
default_scope ->() { where(last_name: "Solo") }
|
277
|
+
end
|
278
|
+
|
279
|
+
expect { run_rake_task }.not_to(change { User.unscoped.count })
|
280
|
+
|
281
|
+
[han, luke].each do |record|
|
282
|
+
expect { record.reload }.to(
|
283
|
+
change { record.last_name }.to("(redacted)")
|
284
|
+
)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
200
288
|
def run_rake_task
|
201
289
|
Rake::Task["db:mask"].execute
|
202
290
|
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
RSpec.describe AttrMasker::Attribute do
|
7
|
+
describe "::new" do
|
8
|
+
subject { described_class.method :new }
|
9
|
+
|
10
|
+
it "instantiates a new attribute definition" do
|
11
|
+
opts = { arbitrary: :options }
|
12
|
+
retval = subject.call(:some_attr, :some_model, opts)
|
13
|
+
expect(retval.name).to eq(:some_attr)
|
14
|
+
expect(retval.model).to eq(:some_model)
|
15
|
+
expect(retval.options).to eq(opts)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#column_name" do
|
20
|
+
subject { receiver.method :column_name }
|
21
|
+
let(:receiver) { described_class.new :some_attr, :some_model, options }
|
22
|
+
let(:options) { {} }
|
23
|
+
|
24
|
+
it "defaults to attribute name" do
|
25
|
+
expect(subject.call).to eq(:some_attr)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can be overriden with :column_name option" do
|
29
|
+
options[:column_name] = :some_column
|
30
|
+
expect(subject.call).to eq(:some_column)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#should_mask?" do
|
35
|
+
subject { described_class.instance_method :should_mask? }
|
36
|
+
|
37
|
+
let(:model_instance) { double }
|
38
|
+
let(:truthy) { double call: true }
|
39
|
+
let(:falsey) { double call: false }
|
40
|
+
|
41
|
+
example { expect(retval_for_opts({})).to be(true) }
|
42
|
+
example { expect(retval_for_opts(if: truthy)).to be(true) }
|
43
|
+
example { expect(retval_for_opts(if: falsey)).to be(false) }
|
44
|
+
example { expect(retval_for_opts(unless: truthy)).to be(false) }
|
45
|
+
example { expect(retval_for_opts(unless: falsey)).to be(true) }
|
46
|
+
example { expect(retval_for_opts(if: truthy, unless: truthy)).to be(false) }
|
47
|
+
example { expect(retval_for_opts(if: truthy, unless: falsey)).to be(true) }
|
48
|
+
example { expect(retval_for_opts(if: falsey, unless: truthy)).to be(false) }
|
49
|
+
example { expect(retval_for_opts(if: falsey, unless: falsey)).to be(false) }
|
50
|
+
|
51
|
+
def retval_for_opts(opts)
|
52
|
+
receiver = described_class.new(:some_attr, :some_model, opts)
|
53
|
+
callable = subject.bind(receiver)
|
54
|
+
callable.(model_instance)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#evaluate_option" do
|
59
|
+
subject { receiver.method :evaluate_option }
|
60
|
+
let(:receiver) { described_class.new :some_attr, model_instance, options }
|
61
|
+
let(:options) { {} }
|
62
|
+
let(:model_instance) { double }
|
63
|
+
let(:retval) { subject.call(:option_name, model_instance) }
|
64
|
+
|
65
|
+
context "when that option value is a symbol" do
|
66
|
+
let(:options) { { option_name: :meth } }
|
67
|
+
|
68
|
+
before do
|
69
|
+
allow(model_instance).to receive(:meth).with(no_args).and_return(:rv)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "evaluates an object's method pointed by that symbol" do
|
73
|
+
expect(retval).to be(:rv)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when that option_nameion value responds to #call" do
|
78
|
+
let(:options) { { option_name: callable } }
|
79
|
+
let(:callable) { double }
|
80
|
+
|
81
|
+
before do
|
82
|
+
allow(callable).to receive(:call).with(model_instance).and_return(:rv)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "calls #call on it passing model instance as the only argument" do
|
86
|
+
expect(retval).to be(:rv)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#marshal_data" do
|
92
|
+
subject { receiver.method :marshal_data }
|
93
|
+
let(:receiver) { described_class.new :some_attr, model_instance, options }
|
94
|
+
let(:options) { { marshaler: marshaller, dump_method: :dump_m } }
|
95
|
+
let(:marshaller) { double }
|
96
|
+
let(:model_instance) { double }
|
97
|
+
|
98
|
+
it "returns unmodified argument when marshal option is falsey" do
|
99
|
+
options[:marshal] = false
|
100
|
+
expect(subject.call(:data)).to be(:data)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns unmodified argument when marshal option is falsey" do
|
104
|
+
options[:marshal] = true
|
105
|
+
expect(marshaller).to receive(:dump_m).with(:data).and_return(:retval)
|
106
|
+
expect(subject.call(:data)).to be(:retval)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#unmarshal_data" do
|
111
|
+
subject { receiver.method :unmarshal_data }
|
112
|
+
let(:receiver) { described_class.new :some_attr, model_instance, options }
|
113
|
+
let(:options) { { marshaler: marshaller, load_method: :load_m } }
|
114
|
+
let(:marshaller) { double }
|
115
|
+
let(:model_instance) { double }
|
116
|
+
|
117
|
+
it "returns unmodified argument when marshal option is falsey" do
|
118
|
+
options[:marshal] = false
|
119
|
+
expect(subject.call(:data)).to be(:data)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "returns unmodified argument when marshal option is falsey" do
|
123
|
+
options[:marshal] = true
|
124
|
+
expect(marshaller).to receive(:load_m).with(:data).and_return(:retval)
|
125
|
+
expect(subject.call(:data)).to be(:retval)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# (c) 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
RSpec.describe AttrMasker::Model do
|
7
|
+
it "extends every class and provides class methods" do
|
8
|
+
c = Class.new
|
9
|
+
expect(c).to respond_to(:attr_masker)
|
10
|
+
expect(c.singleton_class.included_modules).to include(described_class)
|
11
|
+
end
|
12
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_masker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-08-
|
11
|
+
date: 2017-08-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -156,6 +156,7 @@ files:
|
|
156
156
|
- ".rspec"
|
157
157
|
- ".rubocop.yml"
|
158
158
|
- ".travis.yml"
|
159
|
+
- CHANGELOG.adoc
|
159
160
|
- Gemfile
|
160
161
|
- LICENSE
|
161
162
|
- README.adoc
|
@@ -169,9 +170,11 @@ files:
|
|
169
170
|
- gemfiles/Rails-5.1.gemfile
|
170
171
|
- gemfiles/Rails-head.gemfile
|
171
172
|
- lib/attr_masker.rb
|
173
|
+
- lib/attr_masker/attribute.rb
|
172
174
|
- lib/attr_masker/error.rb
|
173
175
|
- lib/attr_masker/maskers/replacing.rb
|
174
176
|
- lib/attr_masker/maskers/simple.rb
|
177
|
+
- lib/attr_masker/model.rb
|
175
178
|
- lib/attr_masker/performer.rb
|
176
179
|
- lib/attr_masker/railtie.rb
|
177
180
|
- lib/attr_masker/version.rb
|
@@ -181,14 +184,16 @@ files:
|
|
181
184
|
- spec/dummy/db/schema.rb
|
182
185
|
- spec/dummy/public/favicon.ico
|
183
186
|
- spec/features_spec.rb
|
184
|
-
- spec/maskers/replacing_spec.rb
|
185
|
-
- spec/maskers/simple_spec.rb
|
186
187
|
- spec/spec_helper.rb
|
187
188
|
- spec/support/0_combustion.rb
|
188
189
|
- spec/support/db_cleaner.rb
|
189
190
|
- spec/support/matchers.rb
|
190
191
|
- spec/support/rake.rb
|
191
192
|
- spec/support/silence_stdout.rb
|
193
|
+
- spec/unit/attribute_spec.rb
|
194
|
+
- spec/unit/maskers/replacing_spec.rb
|
195
|
+
- spec/unit/maskers/simple_spec.rb
|
196
|
+
- spec/unit/model_spec.rb
|
192
197
|
homepage: https://github.com/riboseinc/attr_masker
|
193
198
|
licenses:
|
194
199
|
- MIT
|
@@ -219,11 +224,13 @@ test_files:
|
|
219
224
|
- spec/dummy/db/schema.rb
|
220
225
|
- spec/dummy/public/favicon.ico
|
221
226
|
- spec/features_spec.rb
|
222
|
-
- spec/maskers/replacing_spec.rb
|
223
|
-
- spec/maskers/simple_spec.rb
|
224
227
|
- spec/spec_helper.rb
|
225
228
|
- spec/support/0_combustion.rb
|
226
229
|
- spec/support/db_cleaner.rb
|
227
230
|
- spec/support/matchers.rb
|
228
231
|
- spec/support/rake.rb
|
229
232
|
- spec/support/silence_stdout.rb
|
233
|
+
- spec/unit/attribute_spec.rb
|
234
|
+
- spec/unit/maskers/replacing_spec.rb
|
235
|
+
- spec/unit/maskers/simple_spec.rb
|
236
|
+
- spec/unit/model_spec.rb
|