mongoid 9.0.11 → 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 +65 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/embedded/batchable.rb +8 -0
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/relatable.rb +17 -0
- data/lib/mongoid/clients/factory.rb +2 -0
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/encryption.rb +171 -22
- data/lib/mongoid/config.rb +56 -9
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/encryptable.rb +73 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
- data/lib/mongoid/errors.rb +3 -0
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/indexable.rb +39 -27
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
- data/lib/mongoid/matcher/regex.rb +9 -13
- data/lib/mongoid/matcher/regexp_budget.rb +383 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistence_context.rb +77 -0
- data/lib/mongoid/search_indexable.rb +19 -8
- data/lib/mongoid/tasks/database.rb +33 -16
- data/lib/mongoid/threaded.rb +37 -0
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +7 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/atomic/modifiers_spec.rb +57 -0
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +298 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/attributes/nested_spec.rb +204 -10
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/defaults_spec.rb +18 -0
- data/spec/mongoid/config/encryption_spec.rb +451 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/encryptable_spec.rb +115 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +576 -0
- data/spec/mongoid/search_indexable_spec.rb +34 -1
- data/spec/mongoid/tasks/database_spec.rb +31 -0
- data/spec/support/crypt/models.rb +253 -0
- data/spec/support/models/modifier_models.rb +32 -0
- metadata +17 -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
|
@@ -140,6 +140,32 @@ en:
|
|
|
140
140
|
A collation option is only supported if the query is executed on a MongoDB server
|
|
141
141
|
with version >= 3.4."
|
|
142
142
|
resolution: "Remove the collation option from the query."
|
|
143
|
+
in_memory_regexp_timeout:
|
|
144
|
+
message: "Evaluating this query in memory exceeded the %{limit} second
|
|
145
|
+
limit that applies to conditions containing a regular expression."
|
|
146
|
+
summary: "Mongoid evaluates some conditions in the calling thread
|
|
147
|
+
rather than sending them to the server: queries against an embedded
|
|
148
|
+
association, removing documents from an association by condition,
|
|
149
|
+
and Document#_matches?. A regular expression in such a condition
|
|
150
|
+
runs locally. Mongoid limits the total time one in-memory
|
|
151
|
+
evaluation may spend matching, because both the cost of a single
|
|
152
|
+
match and the number of matches performed grow with the condition,
|
|
153
|
+
and a pattern built from end-user input can be made to consume an
|
|
154
|
+
unbounded amount of CPU. The limit is cumulative over the whole
|
|
155
|
+
evaluation rather than per match. Where the Ruby in use offers no
|
|
156
|
+
per-Regexp timeout the limit is enforced as wall clock over the
|
|
157
|
+
whole in-memory evaluation, so there it can be exceeded by a large
|
|
158
|
+
scan even when the pattern itself is cheap."
|
|
159
|
+
resolution: "Check whether the pattern in this query comes from user
|
|
160
|
+
input; if it does, validate it before querying. If the query is
|
|
161
|
+
legitimate and simply large, raise
|
|
162
|
+
Mongoid::Config.in_memory_regexp_time_limit, or set it to nil to
|
|
163
|
+
remove the limit entirely. The limit in force is the smaller of
|
|
164
|
+
that setting and any global Regexp.timeout the application has set,
|
|
165
|
+
so check both. On JRuby, and on MRI before 3.2, there is no
|
|
166
|
+
per-Regexp timeout, so what the limit bounds there is the elapsed
|
|
167
|
+
time of the whole in-memory evaluation rather than the time spent
|
|
168
|
+
matching."
|
|
143
169
|
invalid_around_callback:
|
|
144
170
|
message: "An around callback must contain a yield in its definition."
|
|
145
171
|
summary: "The block needs to be yielded to for around callbacks to function as intended."
|
|
@@ -213,6 +239,13 @@ en:
|
|
|
213
239
|
Mongoid does not support."
|
|
214
240
|
resolution: "Valid field operators are: %{valid_operators}.
|
|
215
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."
|
|
216
249
|
invalid_estimated_count_criteria:
|
|
217
250
|
message: "Invalid criteria for estimated_count."
|
|
218
251
|
summary: "Estimated count is strictly a collection-level operation and cannot be called
|
|
@@ -515,6 +548,38 @@ en:
|
|
|
515
548
|
environment, Mongoid cannot load its configuration."
|
|
516
549
|
resolution: "Please ensure an environment is set in one of the
|
|
517
550
|
listed locations. The environment must be explicitly set."
|
|
551
|
+
no_encryption_schema:
|
|
552
|
+
message: "The encryption schema of client %{client} does not cover
|
|
553
|
+
%{namespace}."
|
|
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."
|
|
566
|
+
resolution: "Give the model a namespace that is known when the
|
|
567
|
+
client is created, and a client configured with
|
|
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
|
|
571
|
+
chosen at runtime, configure one client per database, each with
|
|
572
|
+
its own auto_encryption_options, and select between them with
|
|
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}."
|
|
518
583
|
no_map_reduce_output:
|
|
519
584
|
message: "No output location was specified for the map/reduce
|
|
520
585
|
operation."
|
|
@@ -12,11 +12,15 @@ module Mongoid
|
|
|
12
12
|
included do
|
|
13
13
|
class_attribute :dependents
|
|
14
14
|
|
|
15
|
+
# Note the leading underscore: without it, ActiveSupport's
|
|
16
|
+
# class_attribute would generate a helper method for this attribute
|
|
17
|
+
# that collides with one it generates for :dependents.
|
|
18
|
+
#
|
|
15
19
|
# @api private
|
|
16
|
-
class_attribute :
|
|
20
|
+
class_attribute :_dependents_owner
|
|
17
21
|
|
|
18
22
|
self.dependents = []
|
|
19
|
-
self.
|
|
23
|
+
self._dependents_owner = self
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
class_methods do
|
|
@@ -57,9 +61,9 @@ module Mongoid
|
|
|
57
61
|
def self.define_dependency!(association)
|
|
58
62
|
validate!(association)
|
|
59
63
|
association.inverse_class.tap do |klass|
|
|
60
|
-
if klass.
|
|
64
|
+
if klass._dependents_owner != klass
|
|
61
65
|
klass.dependents = []
|
|
62
|
-
klass.
|
|
66
|
+
klass._dependents_owner = klass
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
if association.dependent && !klass.dependents.include?(association)
|
|
@@ -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
|
|
@@ -174,7 +174,7 @@ module Mongoid
|
|
|
174
174
|
# @param [ Hash ] attrs The single document attributes to process.
|
|
175
175
|
def update_nested_relation(parent, id, attrs)
|
|
176
176
|
first = existing.first
|
|
177
|
-
converted = first ?
|
|
177
|
+
converted = convert_id(first ? first.class : association.klass, id)
|
|
178
178
|
|
|
179
179
|
if existing.where(_id: converted).exists?
|
|
180
180
|
# document exists in association
|
|
@@ -186,19 +186,48 @@ module Mongoid
|
|
|
186
186
|
end
|
|
187
187
|
elsif association.embedded?
|
|
188
188
|
raise Errors::DocumentNotFound.new(association.klass, id)
|
|
189
|
-
elsif
|
|
190
|
-
|
|
189
|
+
elsif destroyable?(attrs)
|
|
190
|
+
# A destroy of a document that is not in the association is
|
|
191
|
+
# ignored, rather than reaching for it outside the association.
|
|
192
|
+
nil
|
|
193
|
+
elsif Mongoid.allow_reparenting_via_nested_attributes?
|
|
194
|
+
Mongoid::Warnings.warn_reparenting_via_nested_attributes
|
|
191
195
|
|
|
192
196
|
# push existing document to association
|
|
193
197
|
doc = association.klass.unscoped.find(converted)
|
|
198
|
+
# find returns nil instead of raising when
|
|
199
|
+
# Mongoid.raise_not_found_error is false; a missing document
|
|
200
|
+
# must still be an error here, consistent with the other
|
|
201
|
+
# not-found branches.
|
|
202
|
+
raise Errors::DocumentNotFound.new(association.klass, id) if doc.nil?
|
|
203
|
+
|
|
194
204
|
update_document(doc, attrs)
|
|
195
|
-
existing.push(doc)
|
|
205
|
+
existing.push(doc)
|
|
196
206
|
else
|
|
197
|
-
raise Errors::DocumentNotFound.new(association.klass,
|
|
207
|
+
raise Errors::DocumentNotFound.new(association.klass, not_found_params(parent, id))
|
|
198
208
|
end
|
|
199
209
|
|
|
200
210
|
parent.children_may_have_changed!
|
|
201
211
|
end
|
|
212
|
+
|
|
213
|
+
# The params to report when an id in the nested attributes could not
|
|
214
|
+
# be resolved within the association.
|
|
215
|
+
#
|
|
216
|
+
# @api private
|
|
217
|
+
#
|
|
218
|
+
# @param [ Document ] parent The parent document.
|
|
219
|
+
# @param [ String | BSON::ObjectId ] id of the related document.
|
|
220
|
+
#
|
|
221
|
+
# @return [ Hash | Object ] The params for the not found error.
|
|
222
|
+
def not_found_params(parent, id)
|
|
223
|
+
if association.is_a?(Association::Referenced::HasAndBelongsToMany)
|
|
224
|
+
# A many-to-many association has no foreign key on the child, so
|
|
225
|
+
# the id is only ever resolved within the association itself.
|
|
226
|
+
id
|
|
227
|
+
else
|
|
228
|
+
{ _id: id, association.foreign_key => parent.id }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
202
231
|
end
|
|
203
232
|
end
|
|
204
233
|
end
|
|
@@ -58,12 +58,26 @@ module Mongoid
|
|
|
58
58
|
# @example Convert the id.
|
|
59
59
|
# builder.convert_id(Person, "4d371b444835d98b8b000010")
|
|
60
60
|
#
|
|
61
|
+
# Ids arriving from a form are always scalars. A Hash or an Array
|
|
62
|
+
# here means the parameters were crafted, and letting one through
|
|
63
|
+
# would turn the id into a query operator, so they are rejected.
|
|
64
|
+
#
|
|
61
65
|
# @param [ Class ] klass The class we're trying to convert for.
|
|
62
66
|
# @param [ String ] id The id, usually coming from the form.
|
|
63
67
|
#
|
|
64
68
|
# @return [ BSON::ObjectId | String | Object ] The converted id.
|
|
69
|
+
#
|
|
70
|
+
# @raise [ Errors::DocumentNotFound ] if the id is not a scalar, or
|
|
71
|
+
# cannot be converted to the type the class uses for its ids.
|
|
72
|
+
# The BSON::Error rescue is defensive: a value that reaches
|
|
73
|
+
# BSON::ObjectId.mongoize and raises there must not surface as an
|
|
74
|
+
# unhandled BSON error in the caller.
|
|
65
75
|
def convert_id(klass, id)
|
|
76
|
+
raise Errors::DocumentNotFound.new(klass, id) if id.is_a?(::Hash) || id.is_a?(::Array)
|
|
77
|
+
|
|
66
78
|
klass.using_object_ids? ? BSON::ObjectId.mongoize(id) : id
|
|
79
|
+
rescue BSON::Error
|
|
80
|
+
raise Errors::DocumentNotFound.new(klass, id)
|
|
67
81
|
end
|
|
68
82
|
|
|
69
83
|
private
|
|
@@ -534,12 +534,111 @@ module Mongoid
|
|
|
534
534
|
# @return [ Integer ] The number of documents deleted.
|
|
535
535
|
def remove_all(conditions = nil, method = :delete_all)
|
|
536
536
|
selector = conditions || {}
|
|
537
|
-
|
|
537
|
+
selector.merge!(criteria.selector)
|
|
538
|
+
|
|
539
|
+
# Scanning before the delete means scanning only what is already in
|
|
540
|
+
# memory, so it is only done where there is a pattern whose cost has
|
|
541
|
+
# to be bounded. Everywhere else the delete comes first and the scan
|
|
542
|
+
# sees the survivors, which is what this association has always
|
|
543
|
+
# done.
|
|
544
|
+
#
|
|
545
|
+
# The limit is read once and carried into whichever branch is taken.
|
|
546
|
+
# Letting the branch below ask again would read it a second time,
|
|
547
|
+
# across a server round trip, and a limit that had become positive
|
|
548
|
+
# in the meantime would put a deadline on the delete_if -- a query,
|
|
549
|
+
# and an unbind of every match -- which is the arrangement this
|
|
550
|
+
# branch exists to avoid.
|
|
551
|
+
limit = Matcher::RegexpBudget.limit_for(selector)
|
|
552
|
+
return remove_all_bounded(selector, method, limit) if limit
|
|
553
|
+
|
|
554
|
+
removed = klass.send(method, selector)
|
|
555
|
+
|
|
556
|
+
# No scope around this. The scan runs after the delete, so the
|
|
557
|
+
# association this loads comes back holding only what the delete did
|
|
558
|
+
# not match, and there is no pattern here to bound in any case.
|
|
559
|
+
# Opening a scope would save _matches? from deciding once per
|
|
560
|
+
# document -- 0.48us against 1.74us for the match itself -- but a
|
|
561
|
+
# scope with nothing to bound suppresses that decision for
|
|
562
|
+
# everything nested inside it, and loading the association runs
|
|
563
|
+
# find callbacks. A pattern in a selector one of those evaluates
|
|
564
|
+
# would go unguarded, which costs more than the saving is worth on a
|
|
565
|
+
# path that has just made two round trips.
|
|
538
566
|
_target.delete_if do |doc|
|
|
539
567
|
doc._matches?(selector).tap do |b|
|
|
540
568
|
unbind_one(doc) if b
|
|
541
569
|
end
|
|
542
570
|
end
|
|
571
|
+
|
|
572
|
+
removed
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Deletes all related documents matching a selector that carries a
|
|
576
|
+
# regular expression, with the whole scan under one regexp budget.
|
|
577
|
+
#
|
|
578
|
+
# The scan runs before anything is deleted, so a budget that runs out
|
|
579
|
+
# leaves both the database and the association untouched rather than
|
|
580
|
+
# reporting a failure for a delete that has already happened. It also
|
|
581
|
+
# means the budget is closed by the time the association is mutated,
|
|
582
|
+
# so where the budget is enforced with Timeout there is no window for
|
|
583
|
+
# the exception to land in the middle of an unbind.
|
|
584
|
+
#
|
|
585
|
+
# @param [ Hash ] selector The selector to delete with.
|
|
586
|
+
# @param [ Symbol ] method The deletion method to call.
|
|
587
|
+
# @param [ Float ] limit The seconds the scan may spend, as read by
|
|
588
|
+
# the caller when it chose this path.
|
|
589
|
+
#
|
|
590
|
+
# @return [ Integer ] The number of documents deleted.
|
|
591
|
+
def remove_all_bounded(selector, method, limit)
|
|
592
|
+
# Only what the association already holds. Iterating it instead
|
|
593
|
+
# would load the whole association before the delete, and for a
|
|
594
|
+
# broad pattern that is every document the association has: /.*/ is
|
|
595
|
+
# both the cheapest pattern an attacker can supply and the one that
|
|
596
|
+
# matches everything, so bounding the cost of the matching would
|
|
597
|
+
# have been paid for with an unbounded amount of memory. Nothing is
|
|
598
|
+
# missed by not loading, because the server evaluates the same
|
|
599
|
+
# selector for the delete itself.
|
|
600
|
+
#
|
|
601
|
+
# It also means the scan runs no query, so no deadline of ours can
|
|
602
|
+
# land on one. Where the budget is enforced with Timeout, covering
|
|
603
|
+
# a query would report a slow network as a regexp timeout and could
|
|
604
|
+
# deliver the asynchronous exception inside the driver's socket
|
|
605
|
+
# read.
|
|
606
|
+
documents = _target.in_memory
|
|
607
|
+
|
|
608
|
+
matching = Matcher::RegexpBudget.open_with(limit) do
|
|
609
|
+
documents.select { |doc| doc._matches?(selector) }
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
removed = klass.send(method, selector)
|
|
613
|
+
|
|
614
|
+
# The ids are already known to be in memory, so the loaded and added
|
|
615
|
+
# documents can be dropped directly. Enumerable#delete would look
|
|
616
|
+
# each one up by id and stop at the first hash that has it, which
|
|
617
|
+
# removes the wrong instance when both hashes hold one for the same
|
|
618
|
+
# id. Enumerable#delete_if drops it from both, as this does, but
|
|
619
|
+
# only after load_all!, which is the load this scan exists to avoid.
|
|
620
|
+
#
|
|
621
|
+
# Which documents get unbound has always depended on which ones
|
|
622
|
+
# happened to be in memory, and scanning only what is in memory
|
|
623
|
+
# keeps that. It matters for has_and_belongs_to_many, whose
|
|
624
|
+
# unbind_one pulls the id out of the foreign key array on _base and
|
|
625
|
+
# marks it dirty; the scan used to run after the delete, against an
|
|
626
|
+
# association that a cold proxy had just reloaded as holding only
|
|
627
|
+
# the survivors, so nothing matched and nothing was unbound.
|
|
628
|
+
#
|
|
629
|
+
# TODO: unbinding should not depend on what was in memory. Every
|
|
630
|
+
# matched document leaves the association, so every one of them
|
|
631
|
+
# should be unbound, which would also stop _base's foreign key
|
|
632
|
+
# array from keeping ids that no longer resolve. That is a
|
|
633
|
+
# behaviour change for has_and_belongs_to_many and belongs in a
|
|
634
|
+
# ticket of its own, with a release note; it should not ride along
|
|
635
|
+
# on this one.
|
|
636
|
+
matching.each do |doc|
|
|
637
|
+
unbind_one(doc)
|
|
638
|
+
_target._loaded.delete(doc._id)
|
|
639
|
+
_target._added.delete(doc._id)
|
|
640
|
+
end
|
|
641
|
+
|
|
543
642
|
removed
|
|
544
643
|
end
|
|
545
644
|
|
|
@@ -172,6 +172,23 @@ module Mongoid
|
|
|
172
172
|
end
|
|
173
173
|
alias :klass :relation_class
|
|
174
174
|
|
|
175
|
+
# The class of the association target, or nil when the named class is
|
|
176
|
+
# not defined.
|
|
177
|
+
#
|
|
178
|
+
# An association may name a class that never gets defined. The
|
|
179
|
+
# association is then unusable, but its owner still has to be. Callers
|
|
180
|
+
# that walk every association of every model, rather than following the
|
|
181
|
+
# one the application asked for, use this instead of relation_class.
|
|
182
|
+
#
|
|
183
|
+
# @return [ Class | nil ] The association objects' class.
|
|
184
|
+
#
|
|
185
|
+
# @api private
|
|
186
|
+
def try_relation_class
|
|
187
|
+
relation_class
|
|
188
|
+
rescue NameError
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
175
192
|
# The class name of the object owning this association.
|
|
176
193
|
#
|
|
177
194
|
# @return [ String ] The owning objects' class name.
|
|
@@ -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;' +
|
|
@@ -22,6 +22,14 @@ module Mongoid
|
|
|
22
22
|
# @raise [ Errors::CreateCollectionFailure ] If collection creation failed.
|
|
23
23
|
# @raise [ Errors::DropCollectionFailure ] If an attempt to drop collection failed.
|
|
24
24
|
def create_collection(force: false)
|
|
25
|
+
Threaded.with_collection_management do
|
|
26
|
+
perform_create_collection(force: force)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def perform_create_collection(force:)
|
|
25
33
|
if collection_name.empty?
|
|
26
34
|
# This is most probably an anonymous class, we ignore them.
|
|
27
35
|
return
|
|
@@ -21,20 +21,51 @@ module Mongoid
|
|
|
21
21
|
#
|
|
22
22
|
# @return [ Hash ] The encryption schema map.
|
|
23
23
|
def encryption_schema_map(default_database, models = ::Mongoid.models)
|
|
24
|
-
|
|
25
|
-
models
|
|
26
|
-
|
|
27
|
-
|
|
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|
|
|
28
29
|
next if model.embedded?
|
|
29
|
-
next unless model.
|
|
30
|
+
next unless model.requires_encryption_schema?
|
|
30
31
|
|
|
31
32
|
database = model.storage_options.fetch(:database) { default_database }
|
|
33
|
+
# A callable database name cannot be resolved here: the documented
|
|
34
|
+
# multi-tenant idiom has no correct value while the client is being
|
|
35
|
+
# built. Interpolating the callable would produce a key that never
|
|
36
|
+
# matches any namespace, so leave the model out of the map. Writes
|
|
37
|
+
# are refused later, by PersistenceContext, rather than silently
|
|
38
|
+
# going out unencrypted.
|
|
39
|
+
next if database.respond_to?(:call)
|
|
40
|
+
|
|
32
41
|
key = "#{database}.#{model.collection_name}"
|
|
33
|
-
props = metadata_for(model).merge(properties_for(model,
|
|
34
|
-
|
|
42
|
+
props = metadata_for(model).merge(properties_for(model, [ model ], key))
|
|
43
|
+
next if props.empty?
|
|
44
|
+
|
|
45
|
+
# The root of a collection schema describes the document, so it is
|
|
46
|
+
# always an object. Saying so matters when a nested schema carries
|
|
47
|
+
# encryptMetadata: mongocryptd rejects the schema otherwise.
|
|
48
|
+
entry = { 'bsonType' => 'object' }.merge(props)
|
|
49
|
+
map[key] = map.key?(key) ? merge_schema_entries(map[key], entry, key) : entry
|
|
35
50
|
end
|
|
36
51
|
end
|
|
37
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
|
+
|
|
38
69
|
private
|
|
39
70
|
|
|
40
71
|
# The algorithm to use for the deterministic encryption.
|
|
@@ -100,11 +131,14 @@ module Mongoid
|
|
|
100
131
|
# are marked as encrypted.
|
|
101
132
|
#
|
|
102
133
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
103
|
-
# @param [
|
|
134
|
+
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
135
|
+
# inside of, outermost first.
|
|
136
|
+
# @param [ String | nil ] namespace The namespace the schema is built
|
|
137
|
+
# for, used for error reporting.
|
|
104
138
|
#
|
|
105
139
|
# @return [ Hash ] The encryption properties.
|
|
106
|
-
def properties_for(model,
|
|
107
|
-
result = properties_for_fields(model).merge(properties_for_relations(model,
|
|
140
|
+
def properties_for(model, path, namespace = nil)
|
|
141
|
+
result = properties_for_fields(model).merge(properties_for_relations(model, path, namespace))
|
|
108
142
|
if result.empty?
|
|
109
143
|
{}
|
|
110
144
|
else
|
|
@@ -141,26 +175,141 @@ module Mongoid
|
|
|
141
175
|
# are configured to be encrypted.
|
|
142
176
|
#
|
|
143
177
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
144
|
-
# @param [
|
|
178
|
+
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
179
|
+
# inside of, outermost first.
|
|
180
|
+
# @param [ String | nil ] namespace The namespace the schema is built
|
|
181
|
+
# for, used for error reporting.
|
|
145
182
|
#
|
|
146
183
|
# @return [ Hash ] The encryption properties.
|
|
147
|
-
def properties_for_relations(model,
|
|
184
|
+
def properties_for_relations(model, path, namespace = nil)
|
|
148
185
|
model.relations.each_with_object({}) do |(name, relation), props|
|
|
149
|
-
|
|
186
|
+
# relation_class constantizes, and a polymorphic embedded_in has no
|
|
187
|
+
# class to resolve, so the relation type has to be checked first.
|
|
150
188
|
next unless relation.is_a?(Association::Embedded::EmbedsOne)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
|
|
190
|
+
klass = relation.try_relation_class
|
|
191
|
+
# An association target does not have to be a Mongoid document, and
|
|
192
|
+
# the class it names does not have to exist.
|
|
193
|
+
next unless klass.respond_to?(:requires_encryption_schema?)
|
|
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
|
+
)
|
|
160
213
|
end
|
|
214
|
+
props[name] = { 'bsonType' => 'object' }.merge(properties) unless properties.empty?
|
|
161
215
|
end
|
|
162
216
|
end
|
|
163
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
|
+
|
|
164
313
|
# Get the BSON type identifier for the provided field according to the
|
|
165
314
|
# https://www.mongodb.com/docs/manual/reference/bson-types/#std-label-bson-types
|
|
166
315
|
#
|