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.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +91 -0
  3. data/.gitignore +5 -1
  4. data/.rubocop.yml +13 -1069
  5. data/CHANGELOG.adoc +31 -0
  6. data/Gemfile +5 -0
  7. data/README.adoc +81 -30
  8. data/Rakefile +0 -27
  9. data/attr_masker.gemspec +15 -10
  10. data/bin/console +14 -0
  11. data/bin/rake +29 -0
  12. data/bin/rspec +29 -0
  13. data/bin/rubocop +29 -0
  14. data/bin/setup +9 -0
  15. data/gemfiles/Rails-4.2.gemfile +2 -3
  16. data/gemfiles/Rails-5.0.gemfile +2 -3
  17. data/gemfiles/Rails-5.1.gemfile +2 -3
  18. data/gemfiles/Rails-5.2.gemfile +4 -0
  19. data/gemfiles/Rails-6.0.gemfile +3 -0
  20. data/gemfiles/Rails-6.1.gemfile +3 -0
  21. data/gemfiles/Rails-head.gemfile +1 -3
  22. data/gemfiles/common.gemfile +4 -0
  23. data/lib/attr_masker.rb +6 -210
  24. data/lib/attr_masker/attribute.rb +80 -0
  25. data/lib/attr_masker/error.rb +1 -0
  26. data/lib/attr_masker/maskers/replacing.rb +20 -3
  27. data/lib/attr_masker/maskers/simple.rb +20 -5
  28. data/lib/attr_masker/model.rb +143 -0
  29. data/lib/attr_masker/performer.rb +56 -17
  30. data/lib/attr_masker/version.rb +1 -16
  31. data/lib/tasks/db.rake +13 -4
  32. data/spec/dummy/app/models/non_persisted_model.rb +2 -0
  33. data/spec/dummy/config/attr_masker.rb +1 -0
  34. data/spec/dummy/config/mongoid.yml +33 -0
  35. data/spec/dummy/config/routes.rb +0 -1
  36. data/spec/dummy/db/schema.rb +1 -0
  37. data/spec/features/active_record_spec.rb +97 -0
  38. data/spec/features/mongoid_spec.rb +36 -0
  39. data/spec/features/shared_examples.rb +382 -0
  40. data/spec/spec_helper.rb +26 -3
  41. data/spec/support/00_control_constants.rb +2 -0
  42. data/spec/support/10_mongoid_env.rb +9 -0
  43. data/spec/support/20_combustion.rb +10 -0
  44. data/spec/support/db_cleaner.rb +13 -2
  45. data/spec/support/force_config_file_reload.rb +9 -0
  46. data/spec/support/rake.rb +1 -1
  47. data/spec/unit/attribute_spec.rb +210 -0
  48. data/spec/{maskers → unit/maskers}/replacing_spec.rb +0 -0
  49. data/spec/{maskers → unit/maskers}/simple_spec.rb +2 -2
  50. data/spec/unit/model_spec.rb +12 -0
  51. data/spec/unit/rake_task_spec.rb +30 -0
  52. metadata +139 -32
  53. data/.travis.yml +0 -32
  54. data/gemfiles/Rails-4.0.gemfile +0 -5
  55. data/gemfiles/Rails-4.1.gemfile +0 -5
  56. data/spec/features_spec.rb +0 -203
  57. data/spec/support/0_combustion.rb +0 -5
@@ -0,0 +1,4 @@
1
+ eval_gemfile "../Gemfile"
2
+
3
+ gem "codecov", require: false
4
+ gem "simplecov", require: false
data/lib/attr_masker.rb CHANGED
@@ -1,227 +1,23 @@
1
1
  # (c) 2017 Ribose Inc.
2
2
  #
3
3
 
4
+ require "ruby-progressbar"
5
+
4
6
  # Adds attr_accessors that mask an object's attributes
5
7
  module AttrMasker
6
8
  autoload :Version, "attr_masker/version"
9
+ autoload :Attribute, "attr_masker/attribute"
10
+ autoload :Model, "attr_masker/model"
7
11
 
8
12
  autoload :Error, "attr_masker/error"
9
13
  autoload :Performer, "attr_masker/performer"
10
14
 
11
15
  module Maskers
12
16
  autoload :Replacing, "attr_masker/maskers/replacing"
13
- autoload :SIMPLE, "attr_masker/maskers/simple"
17
+ autoload :Simple, "attr_masker/maskers/simple"
14
18
  end
15
19
 
16
20
  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
21
  end
226
22
 
227
- Object.extend AttrMasker
23
+ Object.extend AttrMasker::Model
@@ -0,0 +1,80 @@
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 +false+, 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 = options[:masker]
38
+ masker_value = masker.call(value: value, model: model_instance,
39
+ attribute_name: name, masking_options: options)
40
+ model_instance.send("#{name}=", marshal_data(masker_value))
41
+ end
42
+
43
+ # Returns a hash of maskable attribute names, and respective attribute
44
+ # values. Unchanged attributes are skipped.
45
+ def masked_attributes_new_values(model_instance)
46
+ model_instance.changes.slice(*column_names).transform_values(&:second)
47
+ end
48
+
49
+ # Evaluates option (typically +:if+ or +:unless+) on given model instance.
50
+ # That option can be either a proc (a model is passed as an only argument),
51
+ # or a symbol (a method of that name is called on model instance).
52
+ def evaluate_option(option_name, model_instance)
53
+ option = options[option_name]
54
+
55
+ if option.is_a?(Symbol)
56
+ model_instance.send(option)
57
+ elsif option.respond_to?(:call)
58
+ option.call(model_instance)
59
+ else
60
+ option
61
+ end
62
+ end
63
+
64
+ def marshal_data(data)
65
+ return data unless options[:marshal]
66
+
67
+ options[:marshaler].send(options[:dump_method], data)
68
+ end
69
+
70
+ def unmarshal_data(data)
71
+ return data unless options[:marshal]
72
+
73
+ options[:marshaler].send(options[:load_method], data)
74
+ end
75
+
76
+ def column_names
77
+ options[:column_names] || [name]
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,6 @@
1
1
  # (c) 2017 Ribose Inc.
2
2
  #
3
+
3
4
  module AttrMasker
4
5
  class Error < ::StandardError
5
6
  end
@@ -1,15 +1,32 @@
1
1
  # (c) 2017 Ribose Inc.
2
2
  #
3
+
3
4
  module AttrMasker
4
5
  module Maskers
5
- # This default masker simply replaces any value with a fixed string.
6
+ # +Replacing+ masker replaces every character of string which is being
7
+ # masked with +replacement+ one, preserving the length of the masked string
8
+ # (provided that a replacement string contains a single character, which is
9
+ # a typical case). Optionally, non-alphanumeric characters like dashes or
10
+ # spaces may be left unchanged.
6
11
  #
7
- # +opts+ is a Hash with the key :value that gives you the current attribute
8
- # value.
12
+ # @example Would mask "Adam West" as "XXXXXXXXX"
13
+ # class User < ActiveRecord::Base
14
+ # m = AttrMasker::Maskers::Replacing.new(replacement: "X")
15
+ # attr_masker :name, :masker => m
16
+ # end
9
17
  #
18
+ # @example Would mask "123-456-789" as "XXX-XXX-XXX"
19
+ # class User < ActiveRecord::Base
20
+ # m = AttrMasker::Maskers::Replacing.new(
21
+ # replacement: "X", alphanum_only: true)
22
+ # attr_masker :phone, :masker => m
23
+ # end
10
24
  class Replacing
11
25
  attr_reader :replacement, :alphanum_only
12
26
 
27
+ # @param replacement [String] replacement string
28
+ # @param alphanum_only [Boolean] whether to leave non-alphanumeric
29
+ # characters unchanged or not
13
30
  def initialize(replacement: "*", alphanum_only: false)
14
31
  replacement = "" if replacement.nil?
15
32
  @replacement = replacement
@@ -1,12 +1,27 @@
1
+ # (c) 2017 Ribose Inc.
2
+ #
3
+
1
4
  module AttrMasker
2
5
  module Maskers
3
- # This default masker simply replaces any value with a fixed string.
6
+ # +Simple+ masker replaces values with a predefined +(redacted)+ string.
7
+ # This is a default masker, which is used when no specific +:masker+ is
8
+ # passed in +attr_masker+ method call.
4
9
  #
5
- # +opts+ is a Hash with the key :value that gives you the current attribute
6
- # value.
10
+ # @example Would mask "Adam West" as "(redacted)"
11
+ # class User < ActiveRecord::Base
12
+ # m = AttrMasker::Maskers::Simple.new
13
+ # attr_masker :name, :masker => m
14
+ # end
7
15
  #
8
- SIMPLE = lambda do |_opts|
9
- "(redacted)"
16
+ # @example Would mask "Adam West" as "(redacted)"
17
+ # class User < ActiveRecord::Base
18
+ # attr_masker :name
19
+ # end
20
+ class Simple
21
+ # Accepts any keyword arguments, but they all are ignored.
22
+ def call(**_opts)
23
+ "(redacted)"
24
+ end
10
25
  end
11
26
  end
12
27
  end
@@ -0,0 +1,143 @@
1
+ # (c) 2017 Ribose Inc.
2
+ #
3
+
4
+ module AttrMasker
5
+ module Model
6
+ def self.extended(base) # :nodoc:
7
+ base.class_eval do
8
+ attr_writer :attr_masker_options
9
+ @attr_masker_options = {}
10
+ @masker_attributes = {}
11
+ end
12
+ end
13
+
14
+ # Generates attr_accessors that mask attributes transparently
15
+ #
16
+ # Options (any other options you specify are passed to the masker's mask
17
+ # methods)
18
+ #
19
+ # [:masker]
20
+ # The object to use for masking. It must respond to +#mask+. Defaults to
21
+ # AttrMasker::Maskers::Simple.
22
+ #
23
+ # [:if]
24
+ # Attributes are only masker if this option evaluates to true. If you
25
+ # pass a symbol representing an instance method then the result of
26
+ # the method will be evaluated. Any objects that respond to
27
+ # <tt>:call</tt> are evaluated as well. Defaults to true.
28
+ #
29
+ # [:unless]
30
+ # Attributes are only masker if this option evaluates to false. If you
31
+ # pass a symbol representing an instance method then the result of
32
+ # the method will be evaluated. Any objects that respond to
33
+ # <tt>:call</tt> are evaluated as well. Defaults to false.
34
+ #
35
+ # [:marshal]
36
+ # If set to true, attributes will be marshaled as well as masker. This
37
+ # is useful if you're planning on masking something other than a string.
38
+ # Defaults to false unless you're using it with ActiveRecord or
39
+ # DataMapper.
40
+ #
41
+ # [:marshaler]
42
+ # The object to use for marshaling. Defaults to Marshal.
43
+ #
44
+ # [:dump_method]
45
+ # The dump method name to call on the <tt>:marshaler</tt> object to.
46
+ # Defaults to 'dump'.
47
+ #
48
+ # [:load_method]
49
+ # The load method name to call on the <tt>:marshaler</tt> object.
50
+ # Defaults to 'load'.
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, :another_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
+ #--
80
+ # rubocop:disable Metrics/MethodLength
81
+ def attr_masker(*args)
82
+ default_options = {
83
+ if: true,
84
+ unless: false,
85
+ column_name: nil,
86
+ marshal: false,
87
+ marshaler: Marshal,
88
+ dump_method: "dump",
89
+ load_method: "load",
90
+ masker: AttrMasker::Maskers::Simple.new,
91
+ }
92
+
93
+ options = args.extract_options!.
94
+ reverse_merge(attr_masker_options).
95
+ reverse_merge(default_options)
96
+
97
+ args.each do |attribute_name|
98
+ attribute = Attribute.new(attribute_name, self, options)
99
+ masker_attributes[attribute.name] = attribute
100
+ end
101
+ end
102
+ # rubocop:enable Metrics/MethodLength
103
+
104
+ # Default options to use with calls to <tt>attr_masker</tt>
105
+ # XXX:Keep
106
+ #
107
+ # It will inherit existing options from its superclass
108
+ def attr_masker_options
109
+ @attr_masker_options ||= superclass.attr_masker_options.dup
110
+ end
111
+
112
+ # Checks if an attribute is configured with <tt>attr_masker</tt>
113
+ # XXX:Keep
114
+ #
115
+ # Example
116
+ #
117
+ # class User
118
+ # attr_accessor :name
119
+ # attr_masker :email
120
+ # end
121
+ #
122
+ # User.attr_masker?(:name) # false
123
+ # User.attr_masker?(:email) # true
124
+ def attr_masker?(attribute)
125
+ masker_attributes.has_key?(attribute.to_sym)
126
+ end
127
+
128
+ # Contains a hash of masker attributes with virtual attribute names as keys
129
+ # and their corresponding options as values
130
+ # XXX:Keep
131
+ #
132
+ # Example
133
+ #
134
+ # class User
135
+ # attr_masker :email
136
+ # end
137
+ #
138
+ # User.masker_attributes # { :email => { :attribute => 'masker_email' } }
139
+ def masker_attributes
140
+ @masker_attributes ||= superclass.masker_attributes.dup
141
+ end
142
+ end
143
+ end