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