vault-rails 0.3.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +144 -11
- data/Rakefile +5 -2
- data/lib/vault/encrypted_model.rb +177 -59
- data/lib/vault/rails.rb +77 -29
- data/lib/vault/rails/configurable.rb +54 -8
- data/lib/vault/rails/errors.rb +8 -0
- data/lib/vault/rails/{serializer.rb → json_serializer.rb} +4 -5
- data/lib/vault/rails/version.rb +1 -1
- data/spec/dummy/app/models/lazy_person.rb +20 -0
- data/spec/dummy/app/models/lazy_single_person.rb +18 -0
- data/spec/dummy/app/models/person.rb +36 -1
- 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 +7 -1
- data/spec/dummy/db/schema.rb +21 -16
- data/spec/integration/rails_spec.rb +397 -17
- data/spec/lib/vault/rails/json_serializer_spec.rb +42 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/unit/encrypted_model_spec.rb +9 -4
- data/spec/unit/rails/configurable_spec.rb +118 -0
- data/spec/unit/vault/rails_spec.rb +33 -0
- metadata +55 -56
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -220
- data/spec/dummy/log/test.log +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
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,16 +1,27 @@
|
|
1
|
-
Vault Rails [![Build Status](https://
|
1
|
+
Vault Rails [![Build Status](https://circleci.com/gh/hashicorp/vault-rails.svg?style=shield)](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.**
|
9
|
+
|
10
|
+
## Table of Contents
|
11
|
+
1. [Quick Start](#quick-start)
|
12
|
+
1. [Advanced Configuration](#advanced-configuration)
|
13
|
+
1. [Caveats](#caveats)
|
14
|
+
1. [Development](#development)
|
15
|
+
|
7
16
|
|
8
17
|
Quick Start
|
9
18
|
-----------
|
19
|
+
↥ [back to top](#table-of-contents)
|
20
|
+
|
10
21
|
1. Add to your Gemfile:
|
11
22
|
|
12
23
|
```ruby
|
13
|
-
gem "vault-rails",
|
24
|
+
gem "vault-rails", require: false
|
14
25
|
```
|
15
26
|
|
16
27
|
and then run the `bundle` command to install.
|
@@ -25,13 +36,13 @@ Quick Start
|
|
25
36
|
# disabled, vault-rails will encrypt data in-memory using a similar
|
26
37
|
# algorithm to Vault. The in-memory store uses a predictable encryption
|
27
38
|
# which is great for development and test, but should _never_ be used in
|
28
|
-
# production.
|
39
|
+
# production. Default: ENV["VAULT_RAILS_ENABLED"].
|
29
40
|
vault.enabled = Rails.env.production?
|
30
41
|
|
31
42
|
# The name of the application. All encrypted keys in Vault will be
|
32
43
|
# prefixed with this application name. If you change the name of the
|
33
44
|
# application, you will need to migrate the encrypted data to the new
|
34
|
-
# key namespace.
|
45
|
+
# key namespace. Default: ENV["VAULT_RAILS_APPLICATION"].
|
35
46
|
vault.application = "my_app"
|
36
47
|
|
37
48
|
# The address of the Vault server. Default: ENV["VAULT_ADDR"].
|
@@ -60,7 +71,7 @@ Quick Start
|
|
60
71
|
|
61
72
|
```ruby
|
62
73
|
class AddEncryptedSSNToPerson < ActiveRecord::Migration
|
63
|
-
add_column :
|
74
|
+
add_column :people, :ssn_encrypted, :string
|
64
75
|
end
|
65
76
|
```
|
66
77
|
|
@@ -72,10 +83,13 @@ Quick Start
|
|
72
83
|
person.save #=> true
|
73
84
|
person.ssn_encrypted #=> "vault:v0:EE3EV8P5hyo9h..."
|
74
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)
|
75
87
|
|
76
88
|
|
77
89
|
Advanced Configuration
|
78
90
|
----------------------
|
91
|
+
↥ [back to top](#table-of-contents)
|
92
|
+
|
79
93
|
The following section details some of the more advanced configuration options for vault-rails. As a general rule, you should try to use vault-rails without these options until absolutely necessary.
|
80
94
|
|
81
95
|
#### Specifying the encrypted column
|
@@ -90,7 +104,7 @@ vault_attribute :credit_card,
|
|
90
104
|
- **Note** This value **cannot** be the same name as the vault attribute!
|
91
105
|
|
92
106
|
#### Specifying a custom key
|
93
|
-
By default, the name of the key in Vault is `#{app}_#{table}_#{column}`. This is customizable by setting the `:key`
|
107
|
+
By default, the name of the key in Vault is `#{app}_#{table}_#{column}`. This is customizable by setting the `:key` option when declaring the attribute:
|
94
108
|
|
95
109
|
```ruby
|
96
110
|
vault_attribute :credit_card,
|
@@ -99,7 +113,66 @@ vault_attribute :credit_card,
|
|
99
113
|
|
100
114
|
- **Note** Changing this value for an existing application will make existing values no longer decryptable!
|
101
115
|
|
116
|
+
#### Specifying a context (key derivation)
|
117
|
+
|
118
|
+
Vault Transit supports key derivation, which allows the same key to be used for multiple purposes by deriving a new key based on a context value.
|
119
|
+
|
120
|
+
The context can be specified as a string, symbol, or proc. Symbols (an instance method on the model) and procs are called for each encryption or decryption request, and should return a string.
|
121
|
+
|
122
|
+
- **Note** Changing the context or context generator for an attribute will make existing values no longer decryptable!
|
123
|
+
|
124
|
+
##### String
|
125
|
+
|
126
|
+
With a string, all records will use the same context for this attribute:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
vault_attribute :credit_card,
|
130
|
+
context: "user-cc"
|
131
|
+
```
|
132
|
+
|
133
|
+
##### Symbol
|
134
|
+
|
135
|
+
When using a symbol, a method will be called on the record to compute the context:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
belongs_to :user
|
139
|
+
|
140
|
+
vault_attribute :credit_card,
|
141
|
+
context: :encryption_context
|
142
|
+
|
143
|
+
def encryption_context
|
144
|
+
"user_#{user.id}"
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
##### Proc
|
149
|
+
|
150
|
+
Given a proc, it will be called each time to compute the context:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
belongs_to :user
|
154
|
+
|
155
|
+
vault_attribute :credit_card,
|
156
|
+
context: ->(record) { "user_#{record.user.id}" }
|
157
|
+
```
|
158
|
+
|
159
|
+
The proc must take a single argument for the record.
|
160
|
+
|
161
|
+
#### Specifying a default value
|
162
|
+
|
163
|
+
An attribute can specify a default value, which will be set on initialization (`.new`) or after loading the value from the database. The default will be set if the value is `nil`.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
vault_attribute :access_level,
|
167
|
+
default: "readonly"
|
168
|
+
|
169
|
+
vault_attribute :metadata,
|
170
|
+
serialize: :json,
|
171
|
+
default: {}
|
172
|
+
```
|
173
|
+
|
102
174
|
#### Specifying a different Vault path
|
175
|
+
|
103
176
|
By default, the path to the transit backend in Vault is `transit/`. This is customizable by setting the `:path` option when declaring the attribute:
|
104
177
|
|
105
178
|
```ruby
|
@@ -109,16 +182,72 @@ vault_attribute :credit_card,
|
|
109
182
|
|
110
183
|
- **Note** Changing this value for an existing application will make existing values no longer decryptable!
|
111
184
|
|
112
|
-
####
|
185
|
+
#### Lazy attribute decryption
|
186
|
+
By default, `vault-rails` will decrypt a record’s encrypted attributes on that record’s initialization. You can configure an encrypted model to decrypt attributes lazily, which will prevent communication with Vault until an encrypted attribute’s getter method is called, at which point all of the record’s encrypted attributes will be decrypted. This is useful if you do not always need access to encrypted attributes. For example:
|
187
|
+
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
class Person < ActiveRecord::Base
|
191
|
+
include Vault::EncryptedModel
|
192
|
+
vault_lazy_decrypt!
|
193
|
+
|
194
|
+
vault_attribute :ssn
|
195
|
+
end
|
196
|
+
|
197
|
+
# Without vault_lazy_decrypt:
|
198
|
+
person = Person.find(id) # Vault communication happens here
|
199
|
+
person.ssn
|
200
|
+
# => "123-45-6789"
|
201
|
+
|
202
|
+
# With vault_lazy_decrypt:
|
203
|
+
person = Person.find(id)
|
204
|
+
person.ssn # Vault communication happens here
|
205
|
+
# => "123-45-6789"
|
206
|
+
```
|
207
|
+
|
208
|
+
#### Single, lazy attribute decryption
|
209
|
+
By default, `vault-rails` will decrypt all encrypted attributes on that record’s initialization on a class by class basis. You can configure an encrypted model to decrypt attributes lazily and and individually. This will prevent vault from loading all vault_attributes defined on a class the moment one attribute is requested.
|
210
|
+
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
class Person < ActiveRecord::Base
|
214
|
+
include Vault::EncryptedModel
|
215
|
+
vault_lazy_decrypt!
|
216
|
+
vault_single_decrypt!
|
217
|
+
|
218
|
+
vault_attribute :ssn
|
219
|
+
vault_attribute :email
|
220
|
+
end
|
221
|
+
|
222
|
+
# Without vault_single_decrypt:
|
223
|
+
person = Person.find(id) # Vault communication happens here
|
224
|
+
person.ssn # Vault communication happens here, fetches both ssn and email
|
225
|
+
# => "123-45-6789"
|
226
|
+
|
227
|
+
# With vault_single_decrypt:
|
228
|
+
person = Person.find(id)
|
229
|
+
person.ssn # Vault communication happens here, fetches only ssn
|
230
|
+
# => "123-45-6789"
|
231
|
+
person.email # Vault communication happens here, fetches only email
|
232
|
+
# => "foobar@baz.com"
|
233
|
+
```
|
234
|
+
|
235
|
+
#### Serialization
|
236
|
+
|
113
237
|
By default, all values are assumed to be "text" fields in the database. Sometimes it is beneficial for your application to work with a more flexible data structure (such as a Hash or Array). Vault-rails can automatically serialize and deserialize these structures for you:
|
114
238
|
|
115
239
|
```ruby
|
116
|
-
vault_attribute :details
|
117
|
-
serialize: :json
|
240
|
+
vault_attribute :details,
|
241
|
+
serialize: :json,
|
242
|
+
default: {}
|
118
243
|
```
|
119
244
|
|
245
|
+
It is recommended to set a default matching type that you're serializing.
|
246
|
+
|
120
247
|
- **Note** You can view the source for the exact serialization and deserialization options, but they are intentionally not customizable and cannot be used for a full object marshal/unmarshal.
|
121
248
|
|
249
|
+
##### Custom Serializers
|
250
|
+
|
122
251
|
For customized solutions, you can also pass a module to the `:serializer` key. This module must have the following API:
|
123
252
|
|
124
253
|
```ruby
|
@@ -151,10 +280,12 @@ vault_attribute :address,
|
|
151
280
|
decode: ->(raw) { raw.to_s }
|
152
281
|
```
|
153
282
|
|
154
|
-
- **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to
|
283
|
+
- **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to retrieve existing values!
|
155
284
|
|
156
285
|
Caveats
|
157
286
|
-------
|
287
|
+
↥ [back to top](#table-of-contents)
|
288
|
+
|
158
289
|
|
159
290
|
### Mounting/Creating Keys in Vault
|
160
291
|
The Vault Rails plugin does not automatically mount a backend. It is assumed the proper backend is mounted and accessible by the given token. You can mount a transit backend like this:
|
@@ -217,6 +348,8 @@ the security model).
|
|
217
348
|
|
218
349
|
Development
|
219
350
|
-----------
|
351
|
+
↥ [back to top](#table-of-contents)
|
352
|
+
|
220
353
|
1. Clone the project on GitHub
|
221
354
|
2. Create a feature branch
|
222
355
|
3. Submit a Pull Request
|
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
|
@@ -29,52 +29,55 @@ module Vault
|
|
29
29
|
# the path to the transit backend (default: +transit+)
|
30
30
|
# @option options [String] :key
|
31
31
|
# the name of the encryption key (default: +#{app}_#{table}_#{column}+)
|
32
|
+
# @option options [String, Symbol, Proc] :context
|
33
|
+
# either a string context, or a symbol or proc used to generate a
|
34
|
+
# context for key generation
|
35
|
+
# @option options [Object] :default
|
36
|
+
# a default value for this attribute to be set to if the underlying
|
37
|
+
# value is nil
|
32
38
|
# @option options [Symbol, Class] :serializer
|
33
39
|
# the name of the serializer to use (or a class)
|
34
40
|
# @option options [Proc] :encode
|
35
41
|
# a proc to encode the value with
|
36
42
|
# @option options [Proc] :decode
|
37
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
|
38
47
|
def vault_attribute(attribute, options = {})
|
39
|
-
encrypted_column = options[:encrypted_column] || "#{attribute}_encrypted"
|
40
|
-
path = options[:path] || "transit"
|
41
|
-
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
|
42
|
-
|
43
48
|
# Sanity check options!
|
44
49
|
_vault_validate_options!(options)
|
45
50
|
|
46
|
-
|
47
|
-
|
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"
|
48
57
|
|
49
|
-
#
|
50
|
-
|
51
|
-
if serializer && !serializer.is_a?(Module)
|
52
|
-
serializer = Vault::Rails.serializer_for(serializer)
|
53
|
-
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
|
54
60
|
|
55
|
-
|
56
|
-
|
57
|
-
serializer = Class.new
|
58
|
-
serializer.define_singleton_method(:encode, &options[:encode])
|
59
|
-
serializer.define_singleton_method(:decode, &options[:decode])
|
60
|
-
end
|
61
|
+
self.attribute attribute.to_s, ActiveRecord::Type::Value.new,
|
62
|
+
default: nil
|
61
63
|
|
62
64
|
# Getter
|
63
65
|
define_method("#{attribute}") do
|
64
|
-
self.__vault_load_attributes! unless @__vault_loaded
|
65
|
-
|
66
|
+
self.__vault_load_attributes!(attribute) unless @__vault_loaded
|
67
|
+
super()
|
66
68
|
end
|
67
69
|
|
68
70
|
# Setter
|
69
71
|
define_method("#{attribute}=") do |value|
|
70
|
-
self.__vault_load_attributes! unless @__vault_loaded
|
72
|
+
self.__vault_load_attributes!(attribute) unless @__vault_loaded
|
71
73
|
|
72
74
|
# We always set it as changed without comparing with the current value
|
73
75
|
# because we allow our held values to be mutated, so we need to assume
|
74
|
-
# that if you call attr=, you want it
|
76
|
+
# that if you call attr=, you want it sent back regardless.
|
75
77
|
|
76
78
|
attribute_will_change!("#{attribute}")
|
77
79
|
instance_variable_set("@#{attribute}", value)
|
80
|
+
super(value)
|
78
81
|
|
79
82
|
# Return the value to be consistent with other AR methods.
|
80
83
|
value
|
@@ -82,37 +85,10 @@ module Vault
|
|
82
85
|
|
83
86
|
# Checker
|
84
87
|
define_method("#{attribute}?") do
|
85
|
-
self.__vault_load_attributes! unless @__vault_loaded
|
88
|
+
self.__vault_load_attributes!(attribute) unless @__vault_loaded
|
86
89
|
instance_variable_get("@#{attribute}").present?
|
87
90
|
end
|
88
91
|
|
89
|
-
# Dirty method
|
90
|
-
define_method("#{attribute}_change") do
|
91
|
-
changes["#{attribute}"]
|
92
|
-
end
|
93
|
-
|
94
|
-
# Dirty method
|
95
|
-
define_method("#{attribute}_changed?") do
|
96
|
-
changed.include?("#{attribute}")
|
97
|
-
end
|
98
|
-
|
99
|
-
# Dirty method
|
100
|
-
define_method("#{attribute}_was") do
|
101
|
-
if changes["#{attribute}"]
|
102
|
-
changes["#{attribute}"][0]
|
103
|
-
else
|
104
|
-
public_send("#{attribute}")
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
# Make a note of this attribute so we can use it in the future (maybe).
|
109
|
-
__vault_attributes[attribute.to_sym] = {
|
110
|
-
key: key,
|
111
|
-
path: path,
|
112
|
-
serializer: serializer,
|
113
|
-
encrypted_column: encrypted_column,
|
114
|
-
}
|
115
|
-
|
116
92
|
self
|
117
93
|
end
|
118
94
|
|
@@ -131,6 +107,11 @@ module Vault
|
|
131
107
|
raise Vault::Rails::ValidationFailedError, "Cannot use a " \
|
132
108
|
"custom encoder/decoder if a `:serializer' is specified!"
|
133
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
|
134
115
|
end
|
135
116
|
|
136
117
|
if options[:encode] && !options[:decode]
|
@@ -142,6 +123,19 @@ module Vault
|
|
142
123
|
raise Vault::Rails::ValidationFailedError, "Cannot specify " \
|
143
124
|
"`:decode' without specifying `:encode' as well!"
|
144
125
|
end
|
126
|
+
|
127
|
+
if context = options[:context]
|
128
|
+
if context.is_a?(Proc) && context.arity != 1
|
129
|
+
raise Vault::Rails::ValidationFailedError, "Proc passed to " \
|
130
|
+
"`:context' must take 1 argument!"
|
131
|
+
end
|
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
|
145
139
|
end
|
146
140
|
|
147
141
|
def vault_lazy_decrypt
|
@@ -151,6 +145,62 @@ module Vault
|
|
151
145
|
def vault_lazy_decrypt!
|
152
146
|
@vault_lazy_decrypt = true
|
153
147
|
end
|
148
|
+
|
149
|
+
def vault_single_decrypt
|
150
|
+
@vault_single_decrypt ||= false
|
151
|
+
end
|
152
|
+
|
153
|
+
def vault_single_decrypt!
|
154
|
+
@vault_single_decrypt = true
|
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
|
154
204
|
end
|
155
205
|
|
156
206
|
included do
|
@@ -166,6 +216,12 @@ module Vault
|
|
166
216
|
# before theirs, resulting in attributes that are not persisted.
|
167
217
|
after_save :__vault_persist_attributes!
|
168
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
|
+
|
169
225
|
# Decrypt all the attributes from Vault.
|
170
226
|
# @return [true]
|
171
227
|
def __vault_initialize_attributes!
|
@@ -177,12 +233,16 @@ module Vault
|
|
177
233
|
__vault_load_attributes!
|
178
234
|
end
|
179
235
|
|
180
|
-
def __vault_load_attributes!
|
236
|
+
def __vault_load_attributes!(attribute_to_read = nil)
|
181
237
|
self.class.__vault_attributes.each do |attribute, options|
|
238
|
+
# skip loading certain keys in one of two cases:
|
239
|
+
# 1- the attribute has already been loaded
|
240
|
+
# 2- the single decrypt option is set AND this is not the attribute we're requesting to decrypt
|
241
|
+
next if instance_variable_get("@#{attribute}") || (self.class.vault_single_decrypt && attribute_to_read != attribute)
|
182
242
|
self.__vault_load_attribute!(attribute, options)
|
183
243
|
end
|
184
244
|
|
185
|
-
@__vault_loaded =
|
245
|
+
@__vault_loaded = self.class.__vault_attributes.all? { |attribute, __| instance_variable_defined?("@#{attribute}") }
|
186
246
|
|
187
247
|
return true
|
188
248
|
end
|
@@ -193,26 +253,48 @@ module Vault
|
|
193
253
|
path = options[:path]
|
194
254
|
serializer = options[:serializer]
|
195
255
|
column = options[:encrypted_column]
|
256
|
+
context = options[:context]
|
257
|
+
default = options[:default]
|
258
|
+
transform = options[:transform_secret]
|
196
259
|
|
197
260
|
# Load the ciphertext
|
198
261
|
ciphertext = read_attribute(column)
|
199
262
|
|
200
263
|
# If the user provided a value for the attribute, do not try to load
|
201
264
|
# it from Vault
|
202
|
-
if
|
265
|
+
if attributes[attribute.to_s]
|
203
266
|
return
|
204
267
|
end
|
205
268
|
|
206
|
-
#
|
207
|
-
|
269
|
+
# Generate context if needed
|
270
|
+
generated_context = __vault_generate_context(context)
|
271
|
+
|
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
|
208
284
|
|
209
285
|
# Deserialize the plaintext value, if a serializer exists
|
210
286
|
if serializer
|
211
287
|
plaintext = serializer.decode(plaintext)
|
212
288
|
end
|
213
289
|
|
290
|
+
# Set to default if needed
|
291
|
+
if default && plaintext == nil
|
292
|
+
plaintext = default
|
293
|
+
end
|
294
|
+
|
214
295
|
# Write the virtual attribute with the plaintext value
|
215
296
|
instance_variable_set("@#{attribute}", plaintext)
|
297
|
+
@attributes.write_from_database attribute.to_s, plaintext
|
216
298
|
end
|
217
299
|
|
218
300
|
# Encrypt all the attributes using Vault and set the encrypted values back
|
@@ -244,23 +326,44 @@ module Vault
|
|
244
326
|
path = options[:path]
|
245
327
|
serializer = options[:serializer]
|
246
328
|
column = options[:encrypted_column]
|
329
|
+
context = options[:context]
|
330
|
+
transform = options[:transform_secret]
|
247
331
|
|
248
332
|
# Only persist changed attributes to minimize requests - this helps
|
249
333
|
# minimize the number of requests to Vault.
|
250
|
-
if
|
251
|
-
return
|
334
|
+
if ActiveRecord.gem_version >= Gem::Version.new("6.0")
|
335
|
+
return unless previous_changes.include?(attribute)
|
336
|
+
elsif ActiveRecord.gem_version >= Gem::Version.new("5.2")
|
337
|
+
return unless previous_changes_include?(attribute)
|
338
|
+
elsif ActiveRecord.gem_version >= Gem::Version.new("5.1")
|
339
|
+
return unless saved_change_to_attribute?(attribute.to_s)
|
340
|
+
else
|
341
|
+
return unless attribute_changed?(attribute)
|
252
342
|
end
|
253
343
|
|
254
344
|
# Get the current value of the plaintext attribute
|
255
|
-
plaintext =
|
345
|
+
plaintext = attributes[attribute.to_s]
|
256
346
|
|
257
347
|
# Apply the serialize to the plaintext value, if one exists
|
258
348
|
if serializer
|
259
349
|
plaintext = serializer.encode(plaintext)
|
260
350
|
end
|
261
351
|
|
262
|
-
# Generate
|
263
|
-
|
352
|
+
# Generate context if needed
|
353
|
+
generated_context = __vault_generate_context(context)
|
354
|
+
|
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
|
264
367
|
|
265
368
|
# Write the attribute back, so that we don't have to reload the record
|
266
369
|
# to get the ciphertext
|
@@ -270,6 +373,20 @@ module Vault
|
|
270
373
|
{ column => ciphertext }
|
271
374
|
end
|
272
375
|
|
376
|
+
# Generates an Vault Transit encryption context for use on derived keys.
|
377
|
+
def __vault_generate_context(context)
|
378
|
+
case context
|
379
|
+
when String
|
380
|
+
context
|
381
|
+
when Symbol
|
382
|
+
send(context)
|
383
|
+
when Proc
|
384
|
+
context.call(self)
|
385
|
+
else
|
386
|
+
nil
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
273
390
|
# Override the reload method to reload the Vault attributes. This will
|
274
391
|
# ensure that we always have the most recent data from Vault when we
|
275
392
|
# reload a record from the database.
|
@@ -279,6 +396,7 @@ module Vault
|
|
279
396
|
# from Vault
|
280
397
|
self.class.__vault_attributes.each do |attribute, _|
|
281
398
|
self.instance_variable_set("@#{attribute}", nil)
|
399
|
+
@attributes.write_from_database attribute.to_s, nil
|
282
400
|
end
|
283
401
|
|
284
402
|
self.__vault_initialize_attributes!
|