mongoid 9.0.12 → 9.0.13
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/lib/config/locales/en.yml +31 -11
- data/lib/mongoid/association/embedded/batchable.rb +8 -0
- data/lib/mongoid/clients/factory.rb +2 -0
- data/lib/mongoid/config/encryption.rb +148 -17
- data/lib/mongoid/encryptable.rb +42 -15
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/persistence_context.rb +46 -4
- data/lib/mongoid/search_indexable.rb +5 -1
- data/lib/mongoid/tasks/database.rb +10 -7
- data/lib/mongoid/threaded.rb +1 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/atomic/modifiers_spec.rb +57 -0
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/encryption_spec.rb +223 -0
- data/spec/mongoid/encryptable_spec.rb +54 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +6 -0
- data/spec/mongoid/search_indexable_spec.rb +34 -1
- data/spec/mongoid/tasks/database_spec.rb +13 -0
- data/spec/support/crypt/models.rb +96 -0
- data/spec/support/models/modifier_models.rb +32 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 030d90a4693fc5b3ccb72a7069ce6dc912089351df98fd04ead3595a414433a0
|
|
4
|
+
data.tar.gz: 10362c8758cb9ec1e535b9940eab11088261e7cf4831b8a07bfffb6be231e8df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e5d6bfa84e64cd80f756e55d6fe846f10b5a820c79881b26a54c305f08fb02ef98d27990d7b51b595342844b38c11e04b6e9be911cd3306748c023d226bf9a2
|
|
7
|
+
data.tar.gz: 5a92a5485360f68cea7bbfa48259f0441f0df723d634926230c80b78784e6f8719083b3fef9d81537223b0da8f2b96563c8d267c4bbe9a372c592b781163fe45
|
data/lib/config/locales/en.yml
CHANGED
|
@@ -239,6 +239,13 @@ en:
|
|
|
239
239
|
Mongoid does not support."
|
|
240
240
|
resolution: "Valid field operators are: %{valid_operators}.
|
|
241
241
|
Ensure you are using one of these operators."
|
|
242
|
+
invalid_encryption_schema:
|
|
243
|
+
message: "The encryption schema for %{namespace} is invalid."
|
|
244
|
+
summary: "Two models writing to %{namespace} describe %{path}
|
|
245
|
+
differently. The encryption schema has one entry per namespace,
|
|
246
|
+
so the two descriptions cannot both be used."
|
|
247
|
+
resolution: "Make the models agree on the encryption options of the
|
|
248
|
+
conflicting field, or store them in different collections."
|
|
242
249
|
invalid_estimated_count_criteria:
|
|
243
250
|
message: "Invalid criteria for estimated_count."
|
|
244
251
|
summary: "Estimated count is strictly a collection-level operation and cannot be called
|
|
@@ -544,22 +551,35 @@ en:
|
|
|
544
551
|
no_encryption_schema:
|
|
545
552
|
message: "The encryption schema of client %{client} does not cover
|
|
546
553
|
%{namespace}."
|
|
547
|
-
summary: "%{klass} declares encrypted fields,
|
|
548
|
-
resolved to, %{namespace},
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
554
|
+
summary: "%{klass} declares encrypted fields, directly or through
|
|
555
|
+
an embedded model, but the namespace it resolved to, %{namespace},
|
|
556
|
+
is not part of the automatic encryption schema of client
|
|
557
|
+
%{client}. The encryption schema is built once, when the client
|
|
558
|
+
is created, and is keyed by namespace, so a database name that is
|
|
559
|
+
only known later - a callable :database option,
|
|
560
|
+
Model.with(database:), Mongoid.override_database - is not in it.
|
|
561
|
+
The same happens when an embedded model with encrypted fields was
|
|
562
|
+
not yet loaded when the client was created. Routing the model to a
|
|
563
|
+
client that has no auto_encryption_options has the same effect.
|
|
564
|
+
Mongoid refuses the operation because the driver would store the
|
|
565
|
+
declared fields in plaintext without reporting an error."
|
|
557
566
|
resolution: "Give the model a namespace that is known when the
|
|
558
567
|
client is created, and a client configured with
|
|
559
|
-
auto_encryption_options.
|
|
568
|
+
auto_encryption_options. Load all models, including embedded
|
|
569
|
+
ones, before the first client is created, for example by
|
|
570
|
+
preloading them at boot. If the model has to live in a database
|
|
560
571
|
chosen at runtime, configure one client per database, each with
|
|
561
572
|
its own auto_encryption_options, and select between them with
|
|
562
573
|
Model.with(client:) instead of switching the database."
|
|
574
|
+
no_encryption_schema_field:
|
|
575
|
+
message: "The encryption schema of client %{client} does not cover
|
|
576
|
+
the field %{field} of %{namespace}."
|
|
577
|
+
summary: "The namespace %{namespace} is in the automatic encryption
|
|
578
|
+
schema of client %{client}, but its entry does not encrypt
|
|
579
|
+
%{field}, which %{klass} declares as encrypted. Mongoid refuses
|
|
580
|
+
the operation rather than storing the field in plaintext."
|
|
581
|
+
resolution: "Regenerate the schema map of the client, or add
|
|
582
|
+
%{field} to the schema entry for %{namespace}."
|
|
563
583
|
no_map_reduce_output:
|
|
564
584
|
message: "No output location was specified for the map/reduce
|
|
565
585
|
operation."
|
|
@@ -138,6 +138,14 @@ module Mongoid
|
|
|
138
138
|
_base.collect_children.each do |child|
|
|
139
139
|
child.delayed_atomic_sets.clear
|
|
140
140
|
end
|
|
141
|
+
|
|
142
|
+
# If the base document is itself inserted wholesale via a $set of
|
|
143
|
+
# its atomic path, that $set already contains these documents, and
|
|
144
|
+
# a nested $set under the same path would conflict with it. The
|
|
145
|
+
# cleanup above has already cleared the delayed sets the assigned
|
|
146
|
+
# children recorded, so skipping the recording here is safe.
|
|
147
|
+
return if _base.settable?
|
|
148
|
+
|
|
141
149
|
_base.delayed_atomic_sets[path] = sets
|
|
142
150
|
end
|
|
143
151
|
end
|
|
@@ -91,6 +91,8 @@ module Mongoid
|
|
|
91
91
|
return nil unless opts[:auto_encryption_options]
|
|
92
92
|
|
|
93
93
|
opts[:auto_encryption_options].dup.tap do |auto_encryption_options|
|
|
94
|
+
Mongoid::Warnings.warn_bypass_auto_encryption if auto_encryption_options[:bypass_auto_encryption]
|
|
95
|
+
|
|
94
96
|
if auto_encryption_options.key?(:schema_map)
|
|
95
97
|
default_logger.warn(
|
|
96
98
|
'The :schema_map is configured in the :auto_encryption_options for the client;' +
|
|
@@ -21,7 +21,11 @@ module Mongoid
|
|
|
21
21
|
#
|
|
22
22
|
# @return [ Hash ] The encryption schema map.
|
|
23
23
|
def encryption_schema_map(default_database, models = ::Mongoid.models)
|
|
24
|
-
|
|
24
|
+
# Subclasses share their parent's collection but are not registered in
|
|
25
|
+
# Mongoid.models, so they have to be added to the walk explicitly.
|
|
26
|
+
classes = models.flat_map { |model| [ model ] + model.descendants }.uniq
|
|
27
|
+
|
|
28
|
+
classes.each_with_object({}) do |model, map|
|
|
25
29
|
next if model.embedded?
|
|
26
30
|
next unless model.requires_encryption_schema?
|
|
27
31
|
|
|
@@ -35,14 +39,33 @@ module Mongoid
|
|
|
35
39
|
next if database.respond_to?(:call)
|
|
36
40
|
|
|
37
41
|
key = "#{database}.#{model.collection_name}"
|
|
38
|
-
props = metadata_for(model).merge(properties_for(model, [ model ]))
|
|
42
|
+
props = metadata_for(model).merge(properties_for(model, [ model ], key))
|
|
43
|
+
next if props.empty?
|
|
44
|
+
|
|
39
45
|
# The root of a collection schema describes the document, so it is
|
|
40
46
|
# always an object. Saying so matters when a nested schema carries
|
|
41
47
|
# encryptMetadata: mongocryptd rejects the schema otherwise.
|
|
42
|
-
|
|
48
|
+
entry = { 'bsonType' => 'object' }.merge(props)
|
|
49
|
+
map[key] = map.key?(key) ? merge_schema_entries(map[key], entry, key) : entry
|
|
43
50
|
end
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
# Collect the paths of the fields encrypted by the schema generated for
|
|
54
|
+
# a single model.
|
|
55
|
+
#
|
|
56
|
+
# @param [ Mongoid::Document ] model The model.
|
|
57
|
+
#
|
|
58
|
+
# @return [ Array<Array<String>> ] The property paths, outermost first.
|
|
59
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
def encrypted_paths_for(model)
|
|
62
|
+
return model.instance_variable_get(:@encrypted_paths_for) if
|
|
63
|
+
model.instance_variable_defined?(:@encrypted_paths_for)
|
|
64
|
+
|
|
65
|
+
properties = properties_for(model, [ model ])['properties'] || {}
|
|
66
|
+
model.instance_variable_set(:@encrypted_paths_for, collect_encrypted_paths(properties, []))
|
|
67
|
+
end
|
|
68
|
+
|
|
46
69
|
private
|
|
47
70
|
|
|
48
71
|
# The algorithm to use for the deterministic encryption.
|
|
@@ -110,10 +133,12 @@ module Mongoid
|
|
|
110
133
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
111
134
|
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
112
135
|
# inside of, outermost first.
|
|
136
|
+
# @param [ String | nil ] namespace The namespace the schema is built
|
|
137
|
+
# for, used for error reporting.
|
|
113
138
|
#
|
|
114
139
|
# @return [ Hash ] The encryption properties.
|
|
115
|
-
def properties_for(model, path)
|
|
116
|
-
result = properties_for_fields(model).merge(properties_for_relations(model, path))
|
|
140
|
+
def properties_for(model, path, namespace = nil)
|
|
141
|
+
result = properties_for_fields(model).merge(properties_for_relations(model, path, namespace))
|
|
117
142
|
if result.empty?
|
|
118
143
|
{}
|
|
119
144
|
else
|
|
@@ -152,9 +177,11 @@ module Mongoid
|
|
|
152
177
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
153
178
|
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
154
179
|
# inside of, outermost first.
|
|
180
|
+
# @param [ String | nil ] namespace The namespace the schema is built
|
|
181
|
+
# for, used for error reporting.
|
|
155
182
|
#
|
|
156
183
|
# @return [ Hash ] The encryption properties.
|
|
157
|
-
def properties_for_relations(model, path)
|
|
184
|
+
def properties_for_relations(model, path, namespace = nil)
|
|
158
185
|
model.relations.each_with_object({}) do |(name, relation), props|
|
|
159
186
|
# relation_class constantizes, and a polymorphic embedded_in has no
|
|
160
187
|
# class to resolve, so the relation type has to be checked first.
|
|
@@ -164,21 +191,125 @@ module Mongoid
|
|
|
164
191
|
# An association target does not have to be a Mongoid document, and
|
|
165
192
|
# the class it names does not have to exist.
|
|
166
193
|
next unless klass.respond_to?(:requires_encryption_schema?)
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
|
|
195
|
+
# Any subclass of the declared class can be assigned to the
|
|
196
|
+
# association, so its encrypted fields have to be mapped here too.
|
|
197
|
+
properties = ([ klass ] + klass.descendants).uniq.inject({}) do |merged, candidate|
|
|
198
|
+
# Stop at a model the walk is already inside of, or a self-embedding
|
|
199
|
+
# model never terminates. The path covers the current branch only:
|
|
200
|
+
# a model embedded by two parents, or twice by one parent, has to be
|
|
201
|
+
# emitted at every place it appears.
|
|
202
|
+
next merged if path.include?(candidate)
|
|
203
|
+
next merged unless candidate.respond_to?(:requires_encryption_schema?)
|
|
204
|
+
next merged unless candidate.requires_encryption_schema?
|
|
205
|
+
|
|
206
|
+
merge_schema_entries(
|
|
207
|
+
merged,
|
|
208
|
+
metadata_for(candidate).merge(
|
|
209
|
+
properties_for(candidate, path + [ candidate ], namespace)
|
|
210
|
+
),
|
|
211
|
+
namespace
|
|
212
|
+
)
|
|
178
213
|
end
|
|
214
|
+
props[name] = { 'bsonType' => 'object' }.merge(properties) unless properties.empty?
|
|
179
215
|
end
|
|
180
216
|
end
|
|
181
217
|
|
|
218
|
+
# Walk the schema properties and collect the path of every encrypted
|
|
219
|
+
# leaf.
|
|
220
|
+
#
|
|
221
|
+
# @param [ Hash ] properties The properties to walk.
|
|
222
|
+
# @param [ Array<String> ] path The properties the walk is already
|
|
223
|
+
# inside of.
|
|
224
|
+
#
|
|
225
|
+
# @return [ Array<Array<String>> ] The property paths.
|
|
226
|
+
def collect_encrypted_paths(properties, path)
|
|
227
|
+
properties.each_with_object([]) do |(name, schema), paths|
|
|
228
|
+
if schema.key?('encrypt')
|
|
229
|
+
paths << (path + [ name ])
|
|
230
|
+
elsif schema['properties']
|
|
231
|
+
paths.concat(collect_encrypted_paths(schema['properties'], path + [ name ]))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Merge two schema objects covering the same model or namespace.
|
|
237
|
+
#
|
|
238
|
+
# Every model writing to one namespace contributes an entry to the
|
|
239
|
+
# schema map. Those entries have to agree on everything they have in
|
|
240
|
+
# common, or the schema would silently encrypt a field differently for
|
|
241
|
+
# one model than for another.
|
|
242
|
+
#
|
|
243
|
+
# @param [ Hash ] left The first schema.
|
|
244
|
+
# @param [ Hash ] right The second schema.
|
|
245
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
246
|
+
#
|
|
247
|
+
# @return [ Hash ] The merged schema.
|
|
248
|
+
def merge_schema_entries(left, right, namespace = nil)
|
|
249
|
+
left.merge(right) do |key, left_value, right_value|
|
|
250
|
+
case key
|
|
251
|
+
when 'bsonType'
|
|
252
|
+
left_value
|
|
253
|
+
when 'encryptMetadata'
|
|
254
|
+
merge_encrypt_metadata(left_value, right_value, namespace)
|
|
255
|
+
when 'properties'
|
|
256
|
+
merge_properties(left_value, right_value, namespace)
|
|
257
|
+
else
|
|
258
|
+
raise_invalid_encryption_schema(namespace, key)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Merge two encryptMetadata objects.
|
|
264
|
+
#
|
|
265
|
+
# @param [ Hash ] left The first encryptMetadata object.
|
|
266
|
+
# @param [ Hash ] right The second encryptMetadata object.
|
|
267
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
268
|
+
#
|
|
269
|
+
# @return [ Hash ] The merged encryptMetadata object.
|
|
270
|
+
def merge_encrypt_metadata(left, right, namespace)
|
|
271
|
+
left.merge(right) do |key, left_value, right_value|
|
|
272
|
+
unless left_value == right_value
|
|
273
|
+
raise_invalid_encryption_schema(namespace, "encryptMetadata.#{key}")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
left_value
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Merge two properties objects.
|
|
281
|
+
#
|
|
282
|
+
# @param [ Hash ] left The first properties object.
|
|
283
|
+
# @param [ Hash ] right The second properties object.
|
|
284
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
285
|
+
#
|
|
286
|
+
# @return [ Hash ] The merged properties object.
|
|
287
|
+
def merge_properties(left, right, namespace)
|
|
288
|
+
left.merge(right) do |name, left_value, right_value|
|
|
289
|
+
if left_value.key?('encrypt') && right_value.key?('encrypt')
|
|
290
|
+
unless left_value['encrypt'] == right_value['encrypt']
|
|
291
|
+
raise_invalid_encryption_schema(namespace, "properties.#{name}")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
left_value
|
|
295
|
+
elsif left_value.key?('encrypt') || right_value.key?('encrypt')
|
|
296
|
+
raise_invalid_encryption_schema(namespace, "properties.#{name}")
|
|
297
|
+
else
|
|
298
|
+
merge_schema_entries(left_value, right_value, namespace)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Raise an error for a schema conflicting with another one.
|
|
304
|
+
#
|
|
305
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
306
|
+
# @param [ String ] path The conflicting path in the schema.
|
|
307
|
+
#
|
|
308
|
+
# @raise [ Errors::InvalidEncryptionSchema ] Always raised.
|
|
309
|
+
def raise_invalid_encryption_schema(namespace, path)
|
|
310
|
+
raise Errors::InvalidEncryptionSchema.new(namespace, path)
|
|
311
|
+
end
|
|
312
|
+
|
|
182
313
|
# Get the BSON type identifier for the provided field according to the
|
|
183
314
|
# https://www.mongodb.com/docs/manual/reference/bson-types/#std-label-bson-types
|
|
184
315
|
#
|
data/lib/mongoid/encryptable.rb
CHANGED
|
@@ -44,21 +44,29 @@ module Mongoid
|
|
|
44
44
|
# group has no encrypted field of its own, but its collection still needs
|
|
45
45
|
# a schema, otherwise the embedded fields are written in plaintext.
|
|
46
46
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# not
|
|
47
|
+
# This runs on the persistence path, so the answer is memoized. A true
|
|
48
|
+
# answer is always memoized. A false answer is memoized only when every
|
|
49
|
+
# embeds_one target in the walk resolved to a class; if one could not be
|
|
50
|
+
# resolved yet, the answer is recomputed next time, so a class loaded
|
|
51
|
+
# later is still detected. Declaring encryption on a model after it has
|
|
52
|
+
# already been persisted is not supported.
|
|
50
53
|
#
|
|
51
54
|
# @return [ true | false ] Whether the model needs an encryption schema.
|
|
52
55
|
#
|
|
53
56
|
# @api private
|
|
54
57
|
def requires_encryption_schema?
|
|
55
58
|
return @requires_encryption_schema if defined?(@requires_encryption_schema)
|
|
59
|
+
return @requires_encryption_schema = true if encrypted?
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
found, complete = embedded_encryption_scan([ self ])
|
|
62
|
+
@requires_encryption_schema = found if found || complete
|
|
63
|
+
found
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
#
|
|
61
|
-
# declares encryption.
|
|
66
|
+
# Walks the models reachable through this model's embeds_one relations,
|
|
67
|
+
# looking for one that declares encryption. Subclasses of an
|
|
68
|
+
# association target are included: any of them can be assigned to the
|
|
69
|
+
# association.
|
|
62
70
|
#
|
|
63
71
|
# embeds_many is not considered: libmongocrypt cannot express per-field
|
|
64
72
|
# encryption under array items, so those fields are never mapped.
|
|
@@ -66,21 +74,40 @@ module Mongoid
|
|
|
66
74
|
# @param [ Array<Class> ] path The models the walk is already inside of.
|
|
67
75
|
# A model embedding itself terminates here.
|
|
68
76
|
#
|
|
69
|
-
# @return [ true | false ]
|
|
77
|
+
# @return [ Array<true | false> ] A pair: whether an embedded model is
|
|
78
|
+
# encrypted, and whether every embeds_one target the walk needed
|
|
79
|
+
# resolved to a class. The second value is only meaningful when the
|
|
80
|
+
# first is false.
|
|
70
81
|
#
|
|
71
82
|
# @api private
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
def embedded_encryption_scan(path)
|
|
84
|
+
complete = true
|
|
85
|
+
|
|
86
|
+
relations.each_value do |relation|
|
|
87
|
+
next unless relation.is_a?(Association::Embedded::EmbedsOne)
|
|
75
88
|
|
|
76
89
|
klass = relation.try_relation_class
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
# The class an association names does not have to exist yet.
|
|
91
|
+
if klass.nil?
|
|
92
|
+
complete = false
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# An association target does not have to be a Mongoid document.
|
|
97
|
+
next unless klass.respond_to?(:encrypted?)
|
|
98
|
+
|
|
99
|
+
([ klass ] + klass.descendants).uniq.each do |candidate|
|
|
100
|
+
next if path.include?(candidate)
|
|
101
|
+
return [ true, true ] if candidate.encrypted?
|
|
81
102
|
|
|
82
|
-
|
|
103
|
+
found, nested_complete = candidate.embedded_encryption_scan(path + [ candidate ])
|
|
104
|
+
return [ true, true ] if found
|
|
105
|
+
|
|
106
|
+
complete &&= nested_complete
|
|
107
|
+
end
|
|
83
108
|
end
|
|
109
|
+
|
|
110
|
+
[ false, complete ]
|
|
84
111
|
end
|
|
85
112
|
|
|
86
113
|
# Override the key_id for the model.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Errors
|
|
5
|
+
# This error is raised when two models writing to the same namespace
|
|
6
|
+
# describe a field, or the collection metadata, differently. The schema map
|
|
7
|
+
# has one entry per namespace, so the two descriptions cannot both be
|
|
8
|
+
# honored.
|
|
9
|
+
class InvalidEncryptionSchema < MongoidError
|
|
10
|
+
# Create the new error.
|
|
11
|
+
#
|
|
12
|
+
# @example Create the error.
|
|
13
|
+
# InvalidEncryptionSchema.new('music.bands', 'properties.code')
|
|
14
|
+
#
|
|
15
|
+
# @param [ String | nil ] namespace The namespace being described.
|
|
16
|
+
# @param [ String ] path The conflicting path in the schema.
|
|
17
|
+
def initialize(namespace, path)
|
|
18
|
+
super(
|
|
19
|
+
compose_message(
|
|
20
|
+
'invalid_encryption_schema',
|
|
21
|
+
{ namespace: namespace || 'the collection', path: path }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -15,11 +15,13 @@ module Mongoid
|
|
|
15
15
|
# @param [ Class ] klass The model class.
|
|
16
16
|
# @param [ String ] namespace The namespace the model resolved to.
|
|
17
17
|
# @param [ String | Symbol ] client The name of the client in use.
|
|
18
|
-
|
|
18
|
+
# @param [ String | nil ] field The encrypted field missing from the
|
|
19
|
+
# schema entry, if the namespace itself is covered.
|
|
20
|
+
def initialize(klass, namespace, client, field = nil)
|
|
19
21
|
super(
|
|
20
22
|
compose_message(
|
|
21
|
-
'no_encryption_schema',
|
|
22
|
-
{ klass: klass, namespace: namespace, client: client }
|
|
23
|
+
field ? 'no_encryption_schema_field' : 'no_encryption_schema',
|
|
24
|
+
{ klass: klass, namespace: namespace, client: client, field: field }
|
|
23
25
|
)
|
|
24
26
|
)
|
|
25
27
|
end
|
data/lib/mongoid/errors.rb
CHANGED
|
@@ -20,6 +20,7 @@ require "mongoid/errors/invalid_collection"
|
|
|
20
20
|
require "mongoid/errors/invalid_config_file"
|
|
21
21
|
require "mongoid/errors/invalid_config_option"
|
|
22
22
|
require "mongoid/errors/invalid_dependent_strategy"
|
|
23
|
+
require "mongoid/errors/invalid_encryption_schema"
|
|
23
24
|
require "mongoid/errors/invalid_field"
|
|
24
25
|
require "mongoid/errors/invalid_field_option"
|
|
25
26
|
require "mongoid/errors/invalid_field_type"
|
|
@@ -209,12 +209,15 @@ module Mongoid
|
|
|
209
209
|
# that schema, and when it is absent it asks the server for a schema
|
|
210
210
|
# instead; a collection without a validator then yields no encryption at
|
|
211
211
|
# all, and no error. Checking the namespace here is what turns that silent
|
|
212
|
-
# downgrade into a failure.
|
|
212
|
+
# downgrade into a failure. When the namespace is present, its entry is
|
|
213
|
+
# checked against the fields the model declares as encrypted too: a
|
|
214
|
+
# hand-written schema can cover the namespace and still leave out a field.
|
|
213
215
|
#
|
|
214
216
|
# @param [ Mongo::Client ] client The client this context resolved to.
|
|
215
217
|
#
|
|
216
218
|
# @raise [ Errors::NoEncryptionSchema ] if the model needs an encryption
|
|
217
|
-
# schema and the client's schema does not cover the target namespace
|
|
219
|
+
# schema and the client's schema does not cover the target namespace, or
|
|
220
|
+
# the namespace entry does not cover every encrypted field of the model.
|
|
218
221
|
def verify_encryption_schema!(client)
|
|
219
222
|
klass = @object.is_a?(Class) ? @object : @object.class
|
|
220
223
|
# A model whose encrypted fields all live on embedded models declares no
|
|
@@ -229,9 +232,48 @@ module Mongoid
|
|
|
229
232
|
|
|
230
233
|
schema_map = client.options.dig(:auto_encryption_options, :schema_map)
|
|
231
234
|
namespace = "#{client.database.name}.#{collection_name}"
|
|
232
|
-
|
|
235
|
+
entry = schema_map && schema_map[namespace]
|
|
236
|
+
raise Errors::NoEncryptionSchema.new(klass, namespace, client_name) unless entry
|
|
233
237
|
|
|
234
|
-
|
|
238
|
+
# The schema map may have been written by hand, with symbol keys rather
|
|
239
|
+
# than strings; both are tolerated at every level.
|
|
240
|
+
Mongoid.config.encrypted_paths_for(klass).each do |path|
|
|
241
|
+
next if encryption_schema_has_path?(entry, path)
|
|
242
|
+
|
|
243
|
+
raise Errors::NoEncryptionSchema.new(
|
|
244
|
+
klass, namespace, client_name, path.join('.')
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Whether a schema entry encrypts the property at the given path.
|
|
250
|
+
#
|
|
251
|
+
# @param [ Hash ] entry The namespace schema entry.
|
|
252
|
+
# @param [ Array<String> ] path The property path, outermost first.
|
|
253
|
+
#
|
|
254
|
+
# @return [ true | false ] Whether the leaf is marked as encrypted.
|
|
255
|
+
def encryption_schema_has_path?(entry, path)
|
|
256
|
+
node = entry
|
|
257
|
+
path.each do |name|
|
|
258
|
+
properties = schema_value(node, 'properties')
|
|
259
|
+
return false unless properties.is_a?(Hash)
|
|
260
|
+
|
|
261
|
+
node = schema_value(properties, name)
|
|
262
|
+
return false unless node.is_a?(Hash)
|
|
263
|
+
end
|
|
264
|
+
!schema_value(node, 'encrypt').nil?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Read a schema key that may be written as a string or a symbol.
|
|
268
|
+
#
|
|
269
|
+
# @param [ Hash ] hash The schema object.
|
|
270
|
+
# @param [ String ] key The key to read.
|
|
271
|
+
#
|
|
272
|
+
# @return [ Object ] The value, or nil when the key is absent.
|
|
273
|
+
def schema_value(hash, key)
|
|
274
|
+
return hash[key] if hash.key?(key)
|
|
275
|
+
|
|
276
|
+
hash[key.to_sym]
|
|
235
277
|
end
|
|
236
278
|
|
|
237
279
|
def client_options
|
|
@@ -167,7 +167,11 @@ module Mongoid
|
|
|
167
167
|
#
|
|
168
168
|
# @return [ Array<Hash> ] the raw index documents
|
|
169
169
|
def get_indexes(names)
|
|
170
|
-
|
|
170
|
+
# Reading index metadata sends no document data, so it is exempt from
|
|
171
|
+
# the encryption schema check, like the other index operations here.
|
|
172
|
+
Threaded.with_collection_management do
|
|
173
|
+
collection.search_indexes.select { |i| names.include?(i['name']) }
|
|
174
|
+
end
|
|
171
175
|
end
|
|
172
176
|
end
|
|
173
177
|
end
|
|
@@ -65,15 +65,18 @@ module Mongoid
|
|
|
65
65
|
# @param [ true | false ] wait whether to wait for the indexes to be
|
|
66
66
|
# built.
|
|
67
67
|
def create_search_indexes(models = ::Mongoid.models, wait: true)
|
|
68
|
-
|
|
68
|
+
Threaded.with_collection_management do
|
|
69
|
+
searchable = models.select { |m| m.search_index_specs.any? }
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
# queue up the search index creation requests
|
|
72
|
+
index_names_by_model = searchable.each_with_object({}) do |model, obj|
|
|
73
|
+
logger.info("MONGOID: Creating search indexes on #{model}...")
|
|
74
|
+
obj[model] = model.create_search_indexes
|
|
75
|
+
end
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
wait_for_search_indexes(index_names_by_model) if wait
|
|
78
|
+
index_names_by_model
|
|
79
|
+
end
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
# Return the list of indexes by model that exist in the database but aren't
|
data/lib/mongoid/threaded.rb
CHANGED
data/lib/mongoid/version.rb
CHANGED
data/lib/mongoid/warnings.rb
CHANGED
|
@@ -37,5 +37,9 @@ module Mongoid
|
|
|
37
37
|
warning :autosave_saves_unchanged_documents, "Autosave associations are currently configured to save documents even if they haven't changed. " \
|
|
38
38
|
'This legacy behavior is deprecated. Set Mongoid.autosave_saves_unchanged_documents to false to ' \
|
|
39
39
|
'skip saving unchanged documents in autosave associations.'
|
|
40
|
+
warning :bypass_auto_encryption,
|
|
41
|
+
'auto_encryption_options includes bypass_auto_encryption: true. The driver ' \
|
|
42
|
+
'will not encrypt anything for this client: fields declared with encrypt: true ' \
|
|
43
|
+
'are written to the database unencrypted.'
|
|
40
44
|
end
|
|
41
45
|
end
|