sidekiq-encrypted_args 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f84a1b91b3bfb0404cf7bbbd4465adcb5501bc7ebed09a963bc113db8ceee0ef
4
- data.tar.gz: 87d0e283dd3041834d1cca5b42a575944c8addfaa55c181351b8dee31daa379a
3
+ metadata.gz: 8ca6bc1fc5d72297d12bd0f232a87d4ecaa25979c92eba55ea127822fad621c2
4
+ data.tar.gz: 869bb08e744fa7366e3115a9c1ad7fc8d2d3887a88bee0af7465b0d6b62f5490
5
5
  SHA512:
6
- metadata.gz: 29315f3d29b7b3baaa40093e31418e5ec7b7acdf128f65c0a9be0c3a7dd0dc6fe9fd5d8f84f68dcfa4d4e4054b68e12b78adcb778d901ef63f8673c13dc5671f
7
- data.tar.gz: 1c82169eaa577a5bf375749708381a5f1ac45a1def13e18233f479f491c2927da7e80a8360661b7809fbd418949ad988ba23cb0f580b54f964e7ce02026e0f74
6
+ metadata.gz: 4fc68145cdcfdc34ee2a07cbfd18357a73de96ac5b14a03a4ff09de3376bbba0e501bff181cbda9c964fa79c8e492637e9c7b40c3c4a5cbb82d348bc72be2654
7
+ data.tar.gz: 75418905177a4bea81d7995967d24dbcdb500b4ca6dcf6d528110676569e87dd857b33c04b3fd4aa821cc3200d956b9cd8de037730f08efeebca1cdc60e18378
@@ -1,4 +1,14 @@
1
- # Change log
1
+ # Change Log
2
+
3
+ ## 1.1.0
4
+
5
+ * Use `to_json` if it is defined when serializing encrypted args to JSON.
6
+ * Add client middleware to the server default configuration. This ensures that arguments will be encrypted if a worker enqueues a job with encrypted arguments.
7
+ * Client middleware now reads sidekiq options from the job hash instead of from the worker class so that the list of encrypted arguments is always in sync on the job payload.
8
+ * Don't blow up if class name that is not defined is passed to client middleware.
9
+ * Added additional option to specify encrypted args with array of argument indexes.
10
+ * Deprecated setting encrypted args as hash or array of booleans.
11
+ * Client middleware is prepended while server middleware is appended.
2
12
 
3
13
  ## 1.0.2
4
14
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sidekiq Encrypted Args
2
2
 
3
- ![Continuous Integration](https://github.com/bdurand/sidekiq-encrypted_args/workflows/Continuous%20Integration/badge.svg?branch=master)
3
+ [![Continuous Integration](https://github.com/bdurand/sidekiq-encrypted_args/workflows/Continuous%20Integration/badge.svg?branch=master)](https://github.com/bdurand/sidekiq-encrypted_args/actions?query=workflow%3A%22Continuous+Integration%22)
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/70ab3782e4d5285eb173/maintainability)](https://codeclimate.com/github/bdurand/sidekiq-encrypted_args/maintainability)
5
5
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
6
 
@@ -14,7 +14,7 @@ This can be an even bigger issue if you use scheduled jobs since sensitive data
14
14
 
15
15
  ## Solution
16
16
 
17
- This gem adds Sidekiq middleware that allows you to specify job arguments for your workers that should be encrypted in Redis. You do this by adding `encrypted_args` to the `sidekiq_options` in the worker. Jobs for these workers will have their arguments encrypted in Redis and decrypted when passed to `perform` method.
17
+ This gem adds Sidekiq middleware that allows you to specify job arguments for your workers that should be encrypted in Redis. You do this by adding `encrypted_args` to the `sidekiq_options` in the worker. Jobs for these workers will have their arguments encrypted in Redis and decrypted when passed to the `perform` method.
18
18
 
19
19
  To use the gem, you will need to specify a secret that will be used to encrypt the arguments as well as add the middleware to your Sidekiq client and server middleware stacks. You can set that up by adding this to the end of your Sidekiq initialization:
20
20
 
@@ -24,14 +24,16 @@ Sidekiq::EncryptedArgs.configure!(secret: "YourSecretKey")
24
24
 
25
25
  If the secret is not set, the value of the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` environment variable will be used as the secret. If this variable is not set, job arguments will not be encrypted.
26
26
 
27
- The call to `Sidekiq::EncryptedArgs.configure!` will append the encryption middleware to the end of the client and server middleware chains. You add the middlewares manually if you need more control over where they appear in the stacks.
27
+ The call to `Sidekiq::EncryptedArgs.configure!` will **prepend** the client encryption middleware and **append** server decryption middleware. By doing this, any other middleware you register will only receive the encrypted parameters (e.g. logging middleware will receive the encrypted parameters).
28
+
29
+ You can add the middleware manually if you need more control over where they appear in the stacks.
28
30
 
29
31
  ```ruby
30
32
  Sidekiq::EncryptedArgs.secret = "YourSecretKey"
31
33
 
32
34
  Sidekiq.configure_client do |config|
33
35
  config.client_middleware do |chain|
34
- chain.add Sidekiq::EncryptedArgs::ClientMiddleware
36
+ chain.prepend Sidekiq::EncryptedArgs::ClientMiddleware
35
37
  end
36
38
  end
37
39
 
@@ -39,6 +41,12 @@ Sidekiq.configure_server do |config|
39
41
  config.server_middleware do |chain|
40
42
  chain.add Sidekiq::EncryptedArgs::ServerMiddleware
41
43
  end
44
+
45
+ # register client middleware on the server so that starting jobs in a Sidekiq::Worker also get encrypted args
46
+ # https://github.com/mperham/sidekiq/wiki/Middleware#client-middleware-registered-in-both-places
47
+ config.client_middleware do |chain|
48
+ chain.prepend Sidekiq::EncryptedArgs::ClientMiddleware
49
+ end
42
50
  end
43
51
  ```
44
52
 
@@ -59,30 +67,24 @@ class SecretWorker
59
67
  end
60
68
  ```
61
69
 
62
- You can also encrypt just specific arguments with a hash or an array. This can be useful to preserve visibility into non-sensitive arguments that might be useful for troubleshooting or other reasons. All of these examples will encrypt just the second argument to the `perform` method.
63
-
64
- ```ruby
65
- # Pass in a list of argument names that should be encrypted
66
- sidekiq_options encrypted_args: [:arg_2]
67
-
68
- def perform(arg_1, arg_2, arg_3)
69
- end
70
- ```
70
+ You can also choose to only encrypt specific arguments with an array of either argument names (symbols or strings) or indexes. This is useful to preserve visibility into non-sensitive arguments for troubleshooting or other reasons. Both of these examples encrypt just the second argument to the `perform` method.
71
71
 
72
72
  ```ruby
73
- # Pass in a hash with values indicating which arguments should be encrypted
74
- sidekiq_options encrypted_args: { arg_2: true, arg_1: false }
73
+ # Pass in a list of argument names that should be encrypted
74
+ sidekiq_options encrypted_args: [:arg_2]
75
+ # or
76
+ sidekiq_options encrypted_args: ["arg_2"]
75
77
 
76
- def perform(arg_1, arg_2, arg_3)
77
- end
78
+ def perform(arg_1, arg_2, arg_3)
79
+ end
78
80
  ```
79
81
 
80
82
  ```ruby
81
- # Pass in an array of boolean values indicating which argument positions should be encrypted
82
- sidekiq_options encrypted_args: [false, true]
83
+ # Pass in an array of integers indicating which argument positions should be encrypted
84
+ sidekiq_options encrypted_args: [1]
83
85
 
84
- def perform(arg_1, arg_2, arg_3)
85
- end
86
+ def perform(arg_1, arg_2, arg_3)
87
+ end
86
88
  ```
87
89
 
88
90
  You don't need to change anything else about your workers. All of the arguments passed to the `perform` method will already be unencrypted when the method is called.
@@ -92,15 +94,15 @@ You don't need to change anything else about your workers. All of the arguments
92
94
  If you need to roll your secret, you can simply provide an array when setting the secret.
93
95
 
94
96
  ```ruby
95
- Sidekiq::EncryptedArgs.secret = ["CurrentSecret", "OldSecret"]
97
+ Sidekiq::EncryptedArgs.secret = ["CurrentSecret", "OldSecret", "EvenOlderSecret"]
96
98
  ```
97
99
 
98
- The left most key will be considered the current key and will be used for encrypting arguments. However, all of the keys will be tried when decrypting. This allows you to switch you secret keys without breaking jobs already enqueued in Redis.
100
+ The first (left most) key will be considered the current key, and is used for encrypting arguments. When decrypting, we iterate over the secrets list until we find the correct one. This allows you to switch you secret keys without breaking jobs already enqueued in Redis.
99
101
 
100
- If you are using the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` envrionment variable to specify your secret, you can delimit multiple keys with a spaces.
102
+ If you are using the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` environment variable to specify your secret, you can delimit multiple keys with a spaces.
101
103
 
102
104
  You can also safely add encryption to an existing worker. Any jobs that are already enqueued will still run even without having the arguments encrypted in Redis.
103
105
 
104
106
  ## Encryption
105
107
 
106
- Encrypted arguments are stored using AES-256-GCM with a key derived from your secret using PBKDF2.
108
+ Encrypted arguments are stored using AES-256-GCM with a key derived from your secret using PBKDF2. For more info on the underlying encryption, refer to the [SecretKeys](https://github.com/bdurand/secret_keys) gem.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.2
1
+ 1.1.0
@@ -6,36 +6,44 @@ require "sidekiq"
6
6
 
7
7
  module Sidekiq
8
8
  module EncryptedArgs
9
- # Error thrown when the
9
+ # Error thrown when the secret is invalid
10
10
  class InvalidSecretError < StandardError
11
+ def initialize
12
+ super("Cannot decrypt. Invalid secret provided.")
13
+ end
11
14
  end
12
15
 
13
16
  class << self
14
17
  # Set the secret key used for encrypting arguments. If this is not set,
15
18
  # the value will be loaded from the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` environment
16
- # variable. If that value is not set, arguments will not be encypted.
17
- #
18
- # @param [String] value One or more secrets to use for encypting arguments.
19
+ # variable. If that value is not set, arguments will not be encrypted.
19
20
  #
20
- # @note You can set multiple secrets by passing an array if you need to roll your secrets.
21
+ # You can set multiple secrets by passing an array if you need to roll your secrets.
21
22
  # The left most value in the array will be used as the encryption secret, but
22
23
  # all the values will be tried when decrypting. That way if you have scheduled
23
- # jobs that were encypted with a different secret, you can still make it available
24
+ # jobs that were encrypted with a different secret, you can still make it available
24
25
  # when decrypting the arguments when the job gets run. If you are using the
25
- # envrionment variable, separate the keys with spaces.
26
+ # environment variable, separate the keys with spaces.
27
+ #
28
+ # @param [String] value One or more secrets to use for encrypting arguments.
29
+ # @return [void]
26
30
  def secret=(value)
27
31
  @encryptors = make_encryptors(value)
28
32
  end
29
33
 
30
- # Calling this method will add the client and server middleware to the Sidekiq
34
+ # Add the client and server middleware to the Sidekiq
31
35
  # middleware chains. If you need to ensure the order of where the middleware is
32
36
  # added, you can forgo this method and add it yourself.
37
+ #
38
+ # This method prepends client middleware and appends server middleware.
39
+ #
40
+ # @param [String] secret optionally set the secret here. See {.secret=}
33
41
  def configure!(secret: nil)
34
42
  self.secret = secret unless secret.nil?
35
43
 
36
44
  Sidekiq.configure_client do |config|
37
45
  config.client_middleware do |chain|
38
- chain.add Sidekiq::EncryptedArgs::ClientMiddleware
46
+ chain.prepend Sidekiq::EncryptedArgs::ClientMiddleware
39
47
  end
40
48
  end
41
49
 
@@ -43,17 +51,20 @@ module Sidekiq
43
51
  config.server_middleware do |chain|
44
52
  chain.add Sidekiq::EncryptedArgs::ServerMiddleware
45
53
  end
54
+ config.client_middleware do |chain|
55
+ chain.prepend Sidekiq::EncryptedArgs::ClientMiddleware
56
+ end
46
57
  end
47
58
  end
48
59
 
49
60
  # Encrypt a value.
50
61
  #
51
- # @param [Object] data Data to encrypt. You can pass any JSON compatible data types or structures.
62
+ # @param [#to_json, Object] data Data to encrypt. You can pass any JSON compatible data types or structures.
52
63
  #
53
64
  # @return [String]
54
65
  def encrypt(data)
55
66
  return nil if data.nil?
56
- json = JSON.dump(data)
67
+ json = (data.respond_to?(:to_json) ? data.to_json : JSON.generate(data))
57
68
  encrypted = encrypt_string(json)
58
69
  if encrypted == json
59
70
  data
@@ -67,37 +78,55 @@ module Sidekiq
67
78
  # @param [String] encrypted_data Data that was previously encrypted. If the value passed in is
68
79
  # an unencrypted string, then the string itself will be returned.
69
80
  #
70
- # @return [String]
81
+ # @return [Object]
71
82
  def decrypt(encrypted_data)
72
83
  return encrypted_data unless SecretKeys::Encryptor.encrypted?(encrypted_data)
73
84
  json = decrypt_string(encrypted_data)
74
85
  JSON.parse(json)
75
86
  end
76
87
 
77
- protected
78
-
79
- # Helper method to get the encrypted args option from an options hash. The value of this option
88
+ # Private helper method to get the encrypted args option from an options hash. The value of this option
80
89
  # can be `true` or an array indicating if each positional argument should be encrypted, or a hash
81
90
  # with keys for the argument position and true as the value.
82
- def encrypted_args_option(worker_class)
83
- sidekiq_options = worker_class.sidekiq_options
84
- option = sidekiq_options.fetch(:encrypted_args, sidekiq_options["encrypted_args"])
85
-
91
+ #
92
+ # @private
93
+ def encrypted_args_option(worker_class, job)
94
+ option = job["encrypted_args"]
86
95
  return nil if option.nil?
87
-
88
- return Hash.new(true) if option == true
89
-
90
- return replace_argument_positions(worker_class, option) if option.is_a?(Hash)
91
-
92
- hash = {}
93
- Array(option).each_with_index do |val, position|
94
- if val.is_a?(Symbol) || val.is_a?(String)
95
- hash[val] = true
96
- else
97
- hash[position] = val
96
+ return [] if option == false
97
+
98
+ indexes = []
99
+ if option == true
100
+ job["args"].size.times { |i| indexes << i }
101
+ elsif option.is_a?(Hash)
102
+ deprecation_warning("hash")
103
+ indexes = replace_argument_positions(worker_class, option)
104
+ else
105
+ array_type = nil
106
+ deprecation_message = nil
107
+ Array(option).each_with_index do |val, position|
108
+ current_type = nil
109
+ if val.is_a?(Integer)
110
+ indexes << val
111
+ current_type = :integer
112
+ elsif val.is_a?(Symbol) || val.is_a?(String)
113
+ worker_class = constantize(worker_class) if worker_class.is_a?(String)
114
+ position = perform_method_parameter_index(worker_class, val)
115
+ indexes << position if position
116
+ current_type = :symbol
117
+ else
118
+ deprecation_message = "boolean array"
119
+ indexes << position if val
120
+ end
121
+ if array_type && current_type
122
+ deprecation_message = "array of mixed types"
123
+ else
124
+ array_type ||= current_type
125
+ end
98
126
  end
127
+ deprecation_warning(deprecation_message) if deprecation_message
99
128
  end
100
- replace_argument_positions(worker_class, hash)
129
+ indexes
101
130
  end
102
131
 
103
132
  private
@@ -135,18 +164,40 @@ module Sidekiq
135
164
  Array(secrets).map { |val| val.nil? ? nil : SecretKeys::Encryptor.from_password(val, SALT) }
136
165
  end
137
166
 
138
- def replace_argument_positions(worker_class, encrypt_option)
139
- updated = {}
140
- encrypt_option.each do |key, value|
167
+ def deprecation_warning(message)
168
+ warn("Sidekiq::EncryptedArgs: setting encrypted_args to #{message} is deprecated; support will be removed in version 1.2.")
169
+ end
170
+
171
+ # @param [String] class_name name of a class
172
+ # @return [Class] class that was referenced by name
173
+ def constantize(class_name)
174
+ names = class_name.split("::")
175
+ # Clear leading :: for root namespace since we're already calling from object
176
+ names.shift if names.empty? || names.first.empty?
177
+ # Map reduce to the constant. Use inherit=false to not accidentally search
178
+ # parent modules
179
+ names.inject(Object) { |constant, name| constant.const_get(name, false) }
180
+ end
181
+
182
+ def replace_argument_positions(worker_class, encrypt_option_hash)
183
+ encrypted_indexes = []
184
+ encrypt_option_hash.each do |key, value|
185
+ next unless value
141
186
  if key.is_a?(Symbol) || key.is_a?(String)
142
- key = key.to_sym
143
- position = worker_class.instance_method(:perform).parameters.find_index { |_, name| name == key }
144
- updated[position] = value if position
187
+ position = perform_method_parameter_index(worker_class, key)
188
+ encrypted_indexes << position if position
145
189
  elsif key.is_a?(Integer)
146
- updated[key] = value
190
+ encrypted_indexes << key
147
191
  end
148
192
  end
149
- updated
193
+ encrypted_indexes
194
+ end
195
+
196
+ def perform_method_parameter_index(worker_class, parameter)
197
+ if worker_class
198
+ parameter = parameter.to_sym
199
+ worker_class.instance_method(:perform).parameters.find_index { |_, name| name == parameter }
200
+ end
150
201
  end
151
202
  end
152
203
  end
@@ -5,17 +5,11 @@ module Sidekiq
5
5
  # Sidekiq client middleware for encrypting arguments on jobs for workers
6
6
  # with `encrypted_args` set in the `sidekiq_options`.
7
7
  class ClientMiddleware
8
- # @param [String, Class] worker_class class name or class of worker
8
+ # Encrypt specified arguments before they're sent off to the queue
9
9
  def call(worker_class, job, queue, redis_pool = nil)
10
- worker_class = constantize(worker_class) if worker_class.is_a?(String)
11
- encrypted_args = EncryptedArgs.send(:encrypted_args_option, worker_class)
12
- if encrypted_args
13
- new_args = []
14
- job["args"].each_with_index do |value, position|
15
- value = EncryptedArgs.encrypt(value) if encrypted_args[position]
16
- new_args << value
17
- end
18
- job["args"] = new_args
10
+ if job.include?("encrypted_args")
11
+ encrypted_args = EncryptedArgs.encrypted_args_option(worker_class, job)
12
+ encrypt_job_arguments!(job, encrypted_args)
19
13
  end
20
14
 
21
15
  yield
@@ -23,15 +17,25 @@ module Sidekiq
23
17
 
24
18
  private
25
19
 
26
- # @param [String] class_name name of a class
27
- # @return [Class] class that was referenced by name
28
- def constantize(class_name)
29
- names = class_name.split("::")
30
- # Clear leading :: for root namespace since we're already calling from object
31
- names.shift if names.empty? || names.first.empty?
32
- # Map reduce to the constant. Use inherit=false to not accidentally search
33
- # parent modules
34
- names.inject(Object) { |constant, name| constant.const_get(name, false) }
20
+ # Encrypt the arguments on job
21
+ #
22
+ # Additionally, set `job["encrypted_args"]` to the canonicalized version (i.e. `Array<Integer>`)
23
+ #
24
+ # @param [Hash]
25
+ # @param [Array<Integer>] encrypted_args array of indexes in job to encrypt
26
+ # @return [void]
27
+ def encrypt_job_arguments!(job, encrypted_args)
28
+ if encrypted_args
29
+ job_args = job["args"]
30
+ job_args.each_with_index do |value, position|
31
+ if encrypted_args.include?(position)
32
+ job_args[position] = EncryptedArgs.encrypt(value)
33
+ end
34
+ end
35
+ job["encrypted_args"] = encrypted_args
36
+ else
37
+ job.delete("encrypted_args")
38
+ end
35
39
  end
36
40
  end
37
41
  end
@@ -2,22 +2,33 @@
2
2
 
3
3
  module Sidekiq
4
4
  module EncryptedArgs
5
+ # Sidekiq server middleware for decrypting arguments on jobs that have encrypted args.
5
6
  class ServerMiddleware
6
- # Sidekiq server middleware for encrypting arguments on jobs for workers
7
- # with `encrypted_args` set in the `sidekiq_options`.
7
+ # Wrap the server process to decrypt incoming arguments
8
8
  def call(worker, job, queue)
9
- encrypted_args = EncryptedArgs.send(:encrypted_args_option, worker.class)
9
+ encrypted_args = job["encrypted_args"]
10
10
  if encrypted_args
11
- new_args = []
12
- job["args"].each_with_index do |value, position|
13
- value = EncryptedArgs.decrypt(value) if encrypted_args[position]
14
- new_args << value
11
+ encrypted_args = backward_compatible_encrypted_args(encrypted_args, worker.class, job)
12
+ job_args = job["args"]
13
+ encrypted_args.each do |position|
14
+ value = job_args[position]
15
+ job_args[position] = EncryptedArgs.decrypt(value)
15
16
  end
16
- job["args"] = new_args
17
17
  end
18
18
 
19
19
  yield
20
20
  end
21
+
22
+ private
23
+
24
+ # Ensure that the encrypted args is an array of integers. If not re-read it from the class
25
+ # definition since gem version 1.0.2 and earlier did not update the encrypted_args on the job.
26
+ def backward_compatible_encrypted_args(encrypted_args, worker_class, job)
27
+ unless encrypted_args.is_a?(Array) && encrypted_args.all? { |position| position.is_a?(Integer) }
28
+ encrypted_args = EncryptedArgs.encrypted_args_option(worker_class, job)
29
+ end
30
+ encrypted_args
31
+ end
21
32
  end
22
33
  end
23
34
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-encrypted_args
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
8
  - Winston Durand
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-06-03 00:00:00.000000000 Z
12
+ date: 2020-06-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -53,7 +53,7 @@ dependencies:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '2.0'
56
- description:
56
+ description:
57
57
  email:
58
58
  - bbdurand@gmail.com
59
59
  - me@winstondurand.com
@@ -76,7 +76,7 @@ homepage: https://github.com/bdurand/sidekiq-encrypted_args
76
76
  licenses:
77
77
  - MIT
78
78
  metadata: {}
79
- post_install_message:
79
+ post_install_message:
80
80
  rdoc_options: []
81
81
  require_paths:
82
82
  - lib
@@ -91,8 +91,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubygems_version: 3.0.3
95
- signing_key:
94
+ rubygems_version: 3.1.2
95
+ signing_key:
96
96
  specification_version: 4
97
97
  summary: Support for encrypting arguments that contain sensitive information in sidekiq
98
98
  jobs.