attr_masker 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +91 -0
  3. data/.gitignore +5 -1
  4. data/.rubocop.yml +13 -1069
  5. data/CHANGELOG.adoc +31 -0
  6. data/Gemfile +5 -0
  7. data/README.adoc +81 -30
  8. data/Rakefile +0 -27
  9. data/attr_masker.gemspec +15 -10
  10. data/bin/console +14 -0
  11. data/bin/rake +29 -0
  12. data/bin/rspec +29 -0
  13. data/bin/rubocop +29 -0
  14. data/bin/setup +9 -0
  15. data/gemfiles/Rails-4.2.gemfile +2 -3
  16. data/gemfiles/Rails-5.0.gemfile +2 -3
  17. data/gemfiles/Rails-5.1.gemfile +2 -3
  18. data/gemfiles/Rails-5.2.gemfile +4 -0
  19. data/gemfiles/Rails-6.0.gemfile +3 -0
  20. data/gemfiles/Rails-6.1.gemfile +3 -0
  21. data/gemfiles/Rails-head.gemfile +1 -3
  22. data/gemfiles/common.gemfile +4 -0
  23. data/lib/attr_masker.rb +6 -210
  24. data/lib/attr_masker/attribute.rb +80 -0
  25. data/lib/attr_masker/error.rb +1 -0
  26. data/lib/attr_masker/maskers/replacing.rb +20 -3
  27. data/lib/attr_masker/maskers/simple.rb +20 -5
  28. data/lib/attr_masker/model.rb +143 -0
  29. data/lib/attr_masker/performer.rb +56 -17
  30. data/lib/attr_masker/version.rb +1 -16
  31. data/lib/tasks/db.rake +13 -4
  32. data/spec/dummy/app/models/non_persisted_model.rb +2 -0
  33. data/spec/dummy/config/attr_masker.rb +1 -0
  34. data/spec/dummy/config/mongoid.yml +33 -0
  35. data/spec/dummy/config/routes.rb +0 -1
  36. data/spec/dummy/db/schema.rb +1 -0
  37. data/spec/features/active_record_spec.rb +97 -0
  38. data/spec/features/mongoid_spec.rb +36 -0
  39. data/spec/features/shared_examples.rb +382 -0
  40. data/spec/spec_helper.rb +26 -3
  41. data/spec/support/00_control_constants.rb +2 -0
  42. data/spec/support/10_mongoid_env.rb +9 -0
  43. data/spec/support/20_combustion.rb +10 -0
  44. data/spec/support/db_cleaner.rb +13 -2
  45. data/spec/support/force_config_file_reload.rb +9 -0
  46. data/spec/support/rake.rb +1 -1
  47. data/spec/unit/attribute_spec.rb +210 -0
  48. data/spec/{maskers → unit/maskers}/replacing_spec.rb +0 -0
  49. data/spec/{maskers → unit/maskers}/simple_spec.rb +2 -2
  50. data/spec/unit/model_spec.rb +12 -0
  51. data/spec/unit/rake_task_spec.rb +30 -0
  52. metadata +139 -32
  53. data/.travis.yml +0 -32
  54. data/gemfiles/Rails-4.0.gemfile +0 -5
  55. data/gemfiles/Rails-4.1.gemfile +0 -5
  56. data/spec/features_spec.rb +0 -203
  57. data/spec/support/0_combustion.rb +0 -5
@@ -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