vault-rails 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -3
- data/Rakefile +5 -2
- data/lib/vault/encrypted_model.rb +104 -42
- data/lib/vault/rails.rb +32 -0
- data/lib/vault/rails/configurable.rb +14 -0
- data/lib/vault/rails/version.rb +1 -1
- data/spec/dummy/app/models/person.rb +16 -0
- data/spec/dummy/config/environments/development.rb +5 -3
- data/spec/dummy/config/environments/test.rb +5 -3
- data/spec/dummy/db/migrate/20150428220101_create_people.rb +1 -0
- data/spec/dummy/db/schema.rb +6 -5
- data/spec/integration/rails_spec.rb +109 -16
- data/spec/spec_helper.rb +27 -0
- metadata +48 -54
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -8689
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60cc2b942cbd53163fc7effc3b0f47afc268f203ce079c444635ca9b4f14cc3a
|
|
4
|
+
data.tar.gz: d1c4e963e55205851a0e866eae50067f0a6c4839b53327ae6bb190d6ba98dd7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d08dbab43552537a3380e798efc70ba83694ac6e41bbd3e564c22ae0aead476c9080cf57685b9ac3a4e0719769ff91ce93a72c5bdd64b1a7593700a4306530a8
|
|
7
|
+
data.tar.gz: 594a5b3017457cee3369553b118ab67a105cabc42d7d6b52c4130953d954ec8063b4d52392088ce67f31a07ed6800134d1a5819c18378b312ede905ce9caea69
|
data/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
Vault Rails [](https://circleci.com/gh/hashicorp/vault-rails)
|
|
2
2
|
===========
|
|
3
3
|
|
|
4
4
|
Vault is the official Rails plugin for interacting with [Vault](https://vaultproject.io) by HashiCorp.
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
**If you're viewing this README from GitHub on the `master` branch, know that it may contain unreleased features or
|
|
7
|
+
different APIs than the most recently released version. Please see the Git tag that corresponds to your version of the
|
|
8
|
+
Vault Rails plugin for the proper documentation.**
|
|
7
9
|
|
|
8
10
|
## Table of Contents
|
|
9
11
|
1. [Quick Start](#quick-start)
|
|
@@ -19,7 +21,7 @@ Quick Start
|
|
|
19
21
|
1. Add to your Gemfile:
|
|
20
22
|
|
|
21
23
|
```ruby
|
|
22
|
-
gem "vault-rails",
|
|
24
|
+
gem "vault-rails", require: false
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
and then run the `bundle` command to install.
|
|
@@ -81,6 +83,7 @@ Quick Start
|
|
|
81
83
|
person.save #=> true
|
|
82
84
|
person.ssn_encrypted #=> "vault:v0:EE3EV8P5hyo9h..."
|
|
83
85
|
```
|
|
86
|
+
- **Note** The unencrypted value will still be saved if the attribute referenced has a corresponding column. (i.e. `ssn` in the case above)
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
Advanced Configuration
|
data/Rakefile
CHANGED
|
@@ -11,6 +11,9 @@ Bundler::GemHelper.install_tasks
|
|
|
11
11
|
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
|
12
12
|
load "rails/tasks/engine.rake"
|
|
13
13
|
|
|
14
|
-
require "rspec/core/rake_task"
|
|
15
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
16
14
|
task default: :spec
|
|
15
|
+
|
|
16
|
+
require "rspec/core/rake_task"
|
|
17
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
18
|
+
puts "\n==> Testing with Rails #{Rails::VERSION::STRING} and Ruby #{RUBY_VERSION} <==\n"
|
|
19
|
+
end
|
|
@@ -41,31 +41,22 @@ module Vault
|
|
|
41
41
|
# a proc to encode the value with
|
|
42
42
|
# @option options [Proc] :decode
|
|
43
43
|
# a proc to decode the value with
|
|
44
|
+
# @option options [Hash] :transform_secret
|
|
45
|
+
# a hash providing details about the transformation to use,
|
|
46
|
+
# this includes the name, and the role to use
|
|
44
47
|
def vault_attribute(attribute, options = {})
|
|
45
|
-
encrypted_column = options[:encrypted_column] || "#{attribute}_encrypted"
|
|
46
|
-
path = options[:path] || "transit"
|
|
47
|
-
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
|
|
48
|
-
context = options[:context]
|
|
49
|
-
default = options[:default]
|
|
50
|
-
|
|
51
48
|
# Sanity check options!
|
|
52
49
|
_vault_validate_options!(options)
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
parsed_opts = if options[:transform_secret]
|
|
52
|
+
parse_transform_secret_attributes(attribute, options)
|
|
53
|
+
else
|
|
54
|
+
parse_transit_attributes(attribute, options)
|
|
55
|
+
end
|
|
56
|
+
parsed_opts[:encrypted_column] = options[:encrypted_column] || "#{attribute}_encrypted"
|
|
56
57
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
if serializer && !serializer.is_a?(Module)
|
|
60
|
-
serializer = Vault::Rails.serializer_for(serializer)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# See if custom encoding or decoding options were given.
|
|
64
|
-
if options[:encode] && options[:decode]
|
|
65
|
-
serializer = Class.new
|
|
66
|
-
serializer.define_singleton_method(:encode, &options[:encode])
|
|
67
|
-
serializer.define_singleton_method(:decode, &options[:decode])
|
|
68
|
-
end
|
|
58
|
+
# Make a note of this attribute so we can use it in the future (maybe).
|
|
59
|
+
__vault_attributes[attribute.to_sym] = parsed_opts
|
|
69
60
|
|
|
70
61
|
self.attribute attribute.to_s, ActiveRecord::Type::Value.new,
|
|
71
62
|
default: nil
|
|
@@ -82,7 +73,7 @@ module Vault
|
|
|
82
73
|
|
|
83
74
|
# We always set it as changed without comparing with the current value
|
|
84
75
|
# because we allow our held values to be mutated, so we need to assume
|
|
85
|
-
# that if you call attr=, you want it
|
|
76
|
+
# that if you call attr=, you want it sent back regardless.
|
|
86
77
|
|
|
87
78
|
attribute_will_change!("#{attribute}")
|
|
88
79
|
instance_variable_set("@#{attribute}", value)
|
|
@@ -98,16 +89,6 @@ module Vault
|
|
|
98
89
|
instance_variable_get("@#{attribute}").present?
|
|
99
90
|
end
|
|
100
91
|
|
|
101
|
-
# Make a note of this attribute so we can use it in the future (maybe).
|
|
102
|
-
__vault_attributes[attribute.to_sym] = {
|
|
103
|
-
context: context,
|
|
104
|
-
default: default,
|
|
105
|
-
encrypted_column: encrypted_column,
|
|
106
|
-
key: key,
|
|
107
|
-
path: path,
|
|
108
|
-
serializer: serializer
|
|
109
|
-
}
|
|
110
|
-
|
|
111
92
|
self
|
|
112
93
|
end
|
|
113
94
|
|
|
@@ -126,6 +107,11 @@ module Vault
|
|
|
126
107
|
raise Vault::Rails::ValidationFailedError, "Cannot use a " \
|
|
127
108
|
"custom encoder/decoder if a `:serializer' is specified!"
|
|
128
109
|
end
|
|
110
|
+
|
|
111
|
+
if options[:transform_secret]
|
|
112
|
+
raise Vault::Rails::ValidationFailedError, "Cannot use the " \
|
|
113
|
+
"transform secrets engine with a specified `:serializer'!"
|
|
114
|
+
end
|
|
129
115
|
end
|
|
130
116
|
|
|
131
117
|
if options[:encode] && !options[:decode]
|
|
@@ -144,6 +130,12 @@ module Vault
|
|
|
144
130
|
"`:context' must take 1 argument!"
|
|
145
131
|
end
|
|
146
132
|
end
|
|
133
|
+
if transform_opts = options[:transform_secret]
|
|
134
|
+
if !transform_opts[:transformation]
|
|
135
|
+
raise Vault::Rails::VaildationFailedError, "Transform Secrets " \
|
|
136
|
+
"requires a transformation name!"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
147
139
|
end
|
|
148
140
|
|
|
149
141
|
def vault_lazy_decrypt
|
|
@@ -161,6 +153,54 @@ module Vault
|
|
|
161
153
|
def vault_single_decrypt!
|
|
162
154
|
@vault_single_decrypt = true
|
|
163
155
|
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def parse_transform_secret_attributes(attribute, options)
|
|
160
|
+
opts = {}
|
|
161
|
+
opts[:transform_secret] = true
|
|
162
|
+
|
|
163
|
+
serializer = Class.new
|
|
164
|
+
serializer.define_singleton_method(:encode) do |raw|
|
|
165
|
+
return if raw.nil?
|
|
166
|
+
resp = Vault::Rails.transform_encode(raw, options[:transform_secret])
|
|
167
|
+
resp.dig(:data, :encoded_value)
|
|
168
|
+
end
|
|
169
|
+
serializer.define_singleton_method(:decode) do |raw|
|
|
170
|
+
return if raw.nil?
|
|
171
|
+
resp = Vault::Rails.transform_decode(raw, options[:transform_secret])
|
|
172
|
+
resp.dig(:data, :decoded_value)
|
|
173
|
+
end
|
|
174
|
+
opts[:serializer] = serializer
|
|
175
|
+
opts
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def parse_transit_attributes(attribute, options)
|
|
179
|
+
opts = {}
|
|
180
|
+
opts[:path] = options[:path] || "transit"
|
|
181
|
+
opts[:key] = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
|
|
182
|
+
opts[:context] = options[:context]
|
|
183
|
+
opts[:default] = options[:default]
|
|
184
|
+
|
|
185
|
+
# Get the serializer if one was given.
|
|
186
|
+
serializer = options[:serialize]
|
|
187
|
+
|
|
188
|
+
# Unless a class or module was given, construct our serializer. (Slass
|
|
189
|
+
# is a subset of Module).
|
|
190
|
+
if serializer && !serializer.is_a?(Module)
|
|
191
|
+
serializer = Vault::Rails.serializer_for(serializer)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# See if custom encoding or decoding options were given.
|
|
195
|
+
if options[:encode] && options[:decode]
|
|
196
|
+
serializer = Class.new
|
|
197
|
+
serializer.define_singleton_method(:encode, &options[:encode])
|
|
198
|
+
serializer.define_singleton_method(:decode, &options[:decode])
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
opts[:serializer] = serializer
|
|
202
|
+
opts
|
|
203
|
+
end
|
|
164
204
|
end
|
|
165
205
|
|
|
166
206
|
included do
|
|
@@ -176,6 +216,12 @@ module Vault
|
|
|
176
216
|
# before theirs, resulting in attributes that are not persisted.
|
|
177
217
|
after_save :__vault_persist_attributes!
|
|
178
218
|
|
|
219
|
+
# Before destroying a record, ensure that all vault attributes have been
|
|
220
|
+
# decrypted. Otherwise, attempting to read a lazily decrypted attribute
|
|
221
|
+
# after a destroy will result in a "Can't modify frozen Hash" error when
|
|
222
|
+
# vault-rails attempts to set the plain-text attribute.
|
|
223
|
+
before_destroy :__vault_load_attributes!, unless: -> { @__vault_loaded }
|
|
224
|
+
|
|
179
225
|
# Decrypt all the attributes from Vault.
|
|
180
226
|
# @return [true]
|
|
181
227
|
def __vault_initialize_attributes!
|
|
@@ -196,7 +242,7 @@ module Vault
|
|
|
196
242
|
self.__vault_load_attribute!(attribute, options)
|
|
197
243
|
end
|
|
198
244
|
|
|
199
|
-
@__vault_loaded = self.class.__vault_attributes.all? { |attribute, __|
|
|
245
|
+
@__vault_loaded = self.class.__vault_attributes.all? { |attribute, __| instance_variable_defined?("@#{attribute}") }
|
|
200
246
|
|
|
201
247
|
return true
|
|
202
248
|
end
|
|
@@ -209,6 +255,7 @@ module Vault
|
|
|
209
255
|
column = options[:encrypted_column]
|
|
210
256
|
context = options[:context]
|
|
211
257
|
default = options[:default]
|
|
258
|
+
transform = options[:transform_secret]
|
|
212
259
|
|
|
213
260
|
# Load the ciphertext
|
|
214
261
|
ciphertext = read_attribute(column)
|
|
@@ -222,11 +269,18 @@ module Vault
|
|
|
222
269
|
# Generate context if needed
|
|
223
270
|
generated_context = __vault_generate_context(context)
|
|
224
271
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
272
|
+
if transform
|
|
273
|
+
# If this is a secret encrypted with FPE, we do not need to decrypt with vault
|
|
274
|
+
# This prevents a double encryption via standard vault encryption and FPE.
|
|
275
|
+
# FPE is decrypted later as part of the serializer
|
|
276
|
+
plaintext = ciphertext
|
|
277
|
+
else
|
|
278
|
+
# Load the plaintext value
|
|
279
|
+
plaintext = Vault::Rails.decrypt(
|
|
280
|
+
path, key, ciphertext,
|
|
281
|
+
context: generated_context
|
|
282
|
+
)
|
|
283
|
+
end
|
|
230
284
|
|
|
231
285
|
# Deserialize the plaintext value, if a serializer exists
|
|
232
286
|
if serializer
|
|
@@ -273,6 +327,7 @@ module Vault
|
|
|
273
327
|
serializer = options[:serializer]
|
|
274
328
|
column = options[:encrypted_column]
|
|
275
329
|
context = options[:context]
|
|
330
|
+
transform = options[:transform_secret]
|
|
276
331
|
|
|
277
332
|
# Only persist changed attributes to minimize requests - this helps
|
|
278
333
|
# minimize the number of requests to Vault.
|
|
@@ -297,11 +352,18 @@ module Vault
|
|
|
297
352
|
# Generate context if needed
|
|
298
353
|
generated_context = __vault_generate_context(context)
|
|
299
354
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
355
|
+
if transform
|
|
356
|
+
# If this is a secret encrypted with FPE, we should not encrypt it in vault
|
|
357
|
+
# This prevents a double encryption via standard vault encryption and FPE.
|
|
358
|
+
# FPE was performed earlier as part of the serialization process.
|
|
359
|
+
ciphertext = plaintext
|
|
360
|
+
else
|
|
361
|
+
# Generate the ciphertext and store it back as an attribute
|
|
362
|
+
ciphertext = Vault::Rails.encrypt(
|
|
363
|
+
path, key, plaintext,
|
|
364
|
+
context: generated_context
|
|
365
|
+
)
|
|
366
|
+
end
|
|
305
367
|
|
|
306
368
|
# Write the attribute back, so that we don't have to reload the record
|
|
307
369
|
# to get the ciphertext
|
data/lib/vault/rails.rb
CHANGED
|
@@ -141,6 +141,34 @@ module Vault
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
def transform_encode(plaintext, opts={})
|
|
145
|
+
return plaintext if plaintext&.empty?
|
|
146
|
+
request_opts = {}
|
|
147
|
+
request_opts[:value] = plaintext
|
|
148
|
+
|
|
149
|
+
if opts[:transformation]
|
|
150
|
+
request_opts[:transformation] = opts[:transformation]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
role_name = transform_role_name(opts)
|
|
154
|
+
client.transform.encode(role_name: role_name, **request_opts)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def transform_decode(ciphertext, opts={})
|
|
158
|
+
return ciphertext if ciphertext&.empty?
|
|
159
|
+
request_opts = {}
|
|
160
|
+
request_opts[:value] = ciphertext
|
|
161
|
+
|
|
162
|
+
if opts[:transformation]
|
|
163
|
+
request_opts[:transformation] = opts[:transformation]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
role_name = transform_role_name(opts)
|
|
167
|
+
puts request_opts
|
|
168
|
+
client.transform.decode(role_name: role_name, **request_opts)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
|
|
144
172
|
protected
|
|
145
173
|
|
|
146
174
|
# Perform in-memory encryption. This is useful for testing and development.
|
|
@@ -243,6 +271,10 @@ module Vault
|
|
|
243
271
|
::Rails.logger.warn { msg }
|
|
244
272
|
end
|
|
245
273
|
end
|
|
274
|
+
|
|
275
|
+
def transform_role_name(opts)
|
|
276
|
+
opts[:role] || self.default_role_name || self.application
|
|
277
|
+
end
|
|
246
278
|
end
|
|
247
279
|
end
|
|
248
280
|
end
|
|
@@ -125,6 +125,20 @@ module Vault
|
|
|
125
125
|
def retry_max_wait=(val)
|
|
126
126
|
@retry_max_wait = val
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
# Gets the default role name.
|
|
130
|
+
#
|
|
131
|
+
# @return [String]
|
|
132
|
+
def default_role_name
|
|
133
|
+
@default_role_name
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Sets the default role to use with various plugins.
|
|
137
|
+
#
|
|
138
|
+
# @param [String] val
|
|
139
|
+
def default_role_name=(val)
|
|
140
|
+
@default_role_name = val
|
|
141
|
+
end
|
|
128
142
|
end
|
|
129
143
|
end
|
|
130
144
|
end
|
data/lib/vault/rails/version.rb
CHANGED
|
@@ -38,6 +38,22 @@ class Person < ActiveRecord::Base
|
|
|
38
38
|
vault_attribute :context_proc,
|
|
39
39
|
context: ->(record) { record.encryption_context }
|
|
40
40
|
|
|
41
|
+
vault_attribute :transform_ssn,
|
|
42
|
+
transform_secret: {
|
|
43
|
+
transformation: "social_sec"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
vault_attribute :bad_transform,
|
|
47
|
+
transform_secret: {
|
|
48
|
+
transformation: "foobar_transformation"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
vault_attribute :bad_role_transform,
|
|
52
|
+
transform_secret: {
|
|
53
|
+
transformation: "social_sec",
|
|
54
|
+
role: "foobar_role"
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
def encryption_context
|
|
42
58
|
"user_#{id}"
|
|
43
59
|
end
|
|
@@ -16,9 +16,6 @@ Rails.application.configure do
|
|
|
16
16
|
# Don't care if the mailer can't send.
|
|
17
17
|
config.action_mailer.raise_delivery_errors = false
|
|
18
18
|
|
|
19
|
-
# Print deprecation notices to the Rails logger.
|
|
20
|
-
config.active_support.deprecation = :log
|
|
21
|
-
|
|
22
19
|
# Raise an error on page load if there are pending migrations.
|
|
23
20
|
config.active_record.migration_error = :page_load
|
|
24
21
|
|
|
@@ -36,4 +33,9 @@ Rails.application.configure do
|
|
|
36
33
|
|
|
37
34
|
# Raises error for missing translations
|
|
38
35
|
# config.action_view.raise_on_missing_translations = true
|
|
36
|
+
|
|
37
|
+
# Use native SQLite integers as booleans in Rails 5.2+
|
|
38
|
+
if ActiveRecord.gem_version >= Gem::Version.new("5.2")
|
|
39
|
+
config.active_record.sqlite3.represent_boolean_as_integer = true
|
|
40
|
+
end
|
|
39
41
|
end
|
|
@@ -36,9 +36,11 @@ Rails.application.configure do
|
|
|
36
36
|
# ActionMailer::Base.deliveries array.
|
|
37
37
|
config.action_mailer.delivery_method = :test
|
|
38
38
|
|
|
39
|
-
# Print deprecation notices to the stderr.
|
|
40
|
-
config.active_support.deprecation = :stderr
|
|
41
|
-
|
|
42
39
|
# Raises error for missing translations
|
|
43
40
|
# config.action_view.raise_on_missing_translations = true
|
|
41
|
+
|
|
42
|
+
# Use native SQLite integers as booleans in Rails 5.2+
|
|
43
|
+
if ActiveRecord.gem_version >= Gem::Version.new("5.2")
|
|
44
|
+
config.active_record.sqlite3.represent_boolean_as_integer = true
|
|
45
|
+
end
|
|
44
46
|
end
|
data/spec/dummy/db/schema.rb
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# of editing this file, please use the migrations feature of Active Record to
|
|
3
3
|
# incrementally modify your database, and then regenerate this schema definition.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# from scratch.
|
|
9
|
-
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
10
10
|
#
|
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
|
12
12
|
|
|
@@ -25,6 +25,7 @@ ActiveRecord::Schema.define(version: 2015_04_28_220101) do
|
|
|
25
25
|
t.string "context_string_encrypted"
|
|
26
26
|
t.string "context_symbol_encrypted"
|
|
27
27
|
t.string "context_proc_encrypted"
|
|
28
|
+
t.string "transform_ssn_encrypted"
|
|
28
29
|
t.datetime "created_at", null: false
|
|
29
30
|
t.datetime "updated_at", null: false
|
|
30
31
|
end
|