mongoid 9.1.0 → 9.1.1
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 +49 -0
- data/lib/mongoid/association/accessors.rb +9 -1
- data/lib/mongoid/association/depending.rb +14 -10
- data/lib/mongoid/association/eager.rb +9 -0
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
- data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
- data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
- data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
- data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
- data/lib/mongoid/association/eager_loadable.rb +57 -100
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
- data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
- data/lib/mongoid/association/macros.rb +13 -1
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/nested/one.rb +6 -1
- data/lib/mongoid/association/options.rb +17 -0
- data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
- data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
- data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
- data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
- data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
- data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
- data/lib/mongoid/association/referenced/has_one.rb +1 -0
- data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
- data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
- data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
- data/lib/mongoid/association/referenced.rb +2 -0
- data/lib/mongoid/association/relatable.rb +40 -0
- data/lib/mongoid/association.rb +7 -0
- data/lib/mongoid/clients/factory.rb +7 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +36 -19
- data/lib/mongoid/config.rb +56 -5
- data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/includable.rb +35 -11
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
- data/lib/mongoid/deprecable.rb +45 -2
- data/lib/mongoid/encryptable.rb +46 -0
- data/lib/mongoid/errors/config_redactor.rb +43 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
- data/lib/mongoid/errors/no_client_database.rb +1 -1
- data/lib/mongoid/errors/no_client_hosts.rb +1 -1
- data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +4 -0
- data/lib/mongoid/extensions/vector.rb +64 -0
- data/lib/mongoid/extensions.rb +1 -0
- data/lib/mongoid/field_readable.rb +69 -0
- data/lib/mongoid/indexable.rb +38 -26
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
- data/lib/mongoid/matcher/regex.rb +11 -12
- data/lib/mongoid/matcher/regexp_budget.rb +372 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistable/creatable.rb +1 -1
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +64 -23
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +151 -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/eager_loadable_spec.rb +47 -0
- data/spec/mongoid/association/eager_spec.rb +2 -2
- data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
- data/spec/mongoid/association/fallback_spec.rb +425 -0
- data/spec/mongoid/association/macros_spec.rb +26 -0
- data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
- data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
- data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
- data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
- data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
- data/spec/mongoid/attributes/nested_spec.rb +249 -8
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +228 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/includable_spec.rb +875 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
- data/spec/mongoid/criteria_spec.rb +3 -0
- data/spec/mongoid/encryptable_spec.rb +61 -0
- data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
- data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
- data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
- data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
- data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
- data/spec/mongoid/extensions/vector_spec.rb +175 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +295 -9
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +25 -0
- data/spec/mongoid/touchable_spec_models.rb +10 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +157 -0
- metadata +52 -2
|
@@ -20,18 +20,25 @@ module Mongoid
|
|
|
20
20
|
#
|
|
21
21
|
# @return [ Hash ] The encryption schema map.
|
|
22
22
|
def encryption_schema_map(default_database, models = ::Mongoid.models)
|
|
23
|
-
visited = Set.new
|
|
24
23
|
models.each_with_object({}) do |model, map|
|
|
25
|
-
next if visited.include?(model)
|
|
26
|
-
|
|
27
|
-
visited << model
|
|
28
24
|
next if model.embedded?
|
|
29
|
-
next unless model.
|
|
25
|
+
next unless model.requires_encryption_schema?
|
|
30
26
|
|
|
31
27
|
database = model.storage_options.fetch(:database) { default_database }
|
|
28
|
+
# A callable database name cannot be resolved here: the documented
|
|
29
|
+
# multi-tenant idiom has no correct value while the client is being
|
|
30
|
+
# built. Interpolating the callable would produce a key that never
|
|
31
|
+
# matches any namespace, so leave the model out of the map. Writes
|
|
32
|
+
# are refused later, by PersistenceContext, rather than silently
|
|
33
|
+
# going out unencrypted.
|
|
34
|
+
next if database.respond_to?(:call)
|
|
35
|
+
|
|
32
36
|
key = "#{database}.#{model.collection_name}"
|
|
33
|
-
props = metadata_for(model).merge(properties_for(model,
|
|
34
|
-
|
|
37
|
+
props = metadata_for(model).merge(properties_for(model, [ model ]))
|
|
38
|
+
# The root of a collection schema describes the document, so it is
|
|
39
|
+
# always an object. Saying so matters when a nested schema carries
|
|
40
|
+
# encryptMetadata: mongocryptd rejects the schema otherwise.
|
|
41
|
+
map[key] = { 'bsonType' => 'object' }.merge(props) unless props.empty?
|
|
35
42
|
end
|
|
36
43
|
end
|
|
37
44
|
|
|
@@ -100,11 +107,12 @@ module Mongoid
|
|
|
100
107
|
# are marked as encrypted.
|
|
101
108
|
#
|
|
102
109
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
103
|
-
# @param [
|
|
110
|
+
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
111
|
+
# inside of, outermost first.
|
|
104
112
|
#
|
|
105
113
|
# @return [ Hash ] The encryption properties.
|
|
106
|
-
def properties_for(model,
|
|
107
|
-
result = properties_for_fields(model).merge(properties_for_relations(model,
|
|
114
|
+
def properties_for(model, path)
|
|
115
|
+
result = properties_for_fields(model).merge(properties_for_relations(model, path))
|
|
108
116
|
if result.empty?
|
|
109
117
|
{}
|
|
110
118
|
else
|
|
@@ -141,20 +149,29 @@ module Mongoid
|
|
|
141
149
|
# are configured to be encrypted.
|
|
142
150
|
#
|
|
143
151
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
144
|
-
# @param [
|
|
152
|
+
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
153
|
+
# inside of, outermost first.
|
|
145
154
|
#
|
|
146
155
|
# @return [ Hash ] The encryption properties.
|
|
147
|
-
def properties_for_relations(model,
|
|
156
|
+
def properties_for_relations(model, path)
|
|
148
157
|
model.relations.each_with_object({}) do |(name, relation), props|
|
|
149
|
-
|
|
158
|
+
# relation_class constantizes, and a polymorphic embedded_in has no
|
|
159
|
+
# class to resolve, so the relation type has to be checked first.
|
|
150
160
|
next unless relation.is_a?(Association::Embedded::EmbedsOne)
|
|
151
|
-
next unless relation.relation_class.encrypted?
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
klass = relation.try_relation_class
|
|
163
|
+
# An association target does not have to be a Mongoid document, and
|
|
164
|
+
# the class it names does not have to exist.
|
|
165
|
+
next unless klass.respond_to?(:requires_encryption_schema?)
|
|
166
|
+
# Stop at a model the walk is already inside of, or a self-embedding
|
|
167
|
+
# model never terminates. The path covers the current branch only:
|
|
168
|
+
# a model embedded by two parents, or twice by one parent, has to be
|
|
169
|
+
# emitted at every place it appears.
|
|
170
|
+
next if path.include?(klass)
|
|
171
|
+
next unless klass.requires_encryption_schema?
|
|
172
|
+
|
|
173
|
+
metadata_for(klass).merge(
|
|
174
|
+
properties_for(klass, path + [ klass ])
|
|
158
175
|
).tap do |properties|
|
|
159
176
|
props[name] = { 'bsonType' => 'object' }.merge(properties) unless properties.empty?
|
|
160
177
|
end
|
data/lib/mongoid/config.rb
CHANGED
|
@@ -130,12 +130,14 @@ module Mongoid
|
|
|
130
130
|
validate_isolation_level!(level)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
# When this flag is true, it will be possible to
|
|
134
|
-
#
|
|
135
|
-
# nested attributes for another parent record
|
|
133
|
+
# When this flag is true, it will be possible to add a record to a
|
|
134
|
+
# "has_many" or "has_and_belongs_to_many" association by passing that
|
|
135
|
+
# record's id in the nested attributes for another parent record, even
|
|
136
|
+
# when the record does not already belong to that association. For a
|
|
137
|
+
# "has_many" association this moves the record to the new parent.
|
|
136
138
|
#
|
|
137
|
-
# When this flag is false,
|
|
138
|
-
#
|
|
139
|
+
# When this flag is false, an id in nested attributes is only resolved
|
|
140
|
+
# within the association itself, and anything else raises an error.
|
|
139
141
|
#
|
|
140
142
|
# The default is `false`. Note that allowing reparenting via nested attributes
|
|
141
143
|
# is a potential security risk, since it could allow a malicious user to move
|
|
@@ -294,6 +296,51 @@ module Mongoid
|
|
|
294
296
|
# See https://jira.mongodb.org/browse/MONGOID-5892 for more details.
|
|
295
297
|
option :serializable_hash_with_legacy_only, default: true
|
|
296
298
|
|
|
299
|
+
# When false (default), query operators are restricted when building a
|
|
300
|
+
# selector, so that user-supplied input reaching the query builder cannot
|
|
301
|
+
# make MongoDB execute arbitrary JavaScript. Two rules are enforced:
|
|
302
|
+
#
|
|
303
|
+
# - An operator at the top level of an expression must appear in
|
|
304
|
+
# +Criteria::Queryable::Selectable::ALLOWED_QUERY_OPERATORS+.
|
|
305
|
+
# - +$where+, +$function+, and +$accumulator+ are rejected at any depth,
|
|
306
|
+
# including inside +$expr+ and the logical operators.
|
|
307
|
+
#
|
|
308
|
+
# This applies to every query method that accepts an expression, including
|
|
309
|
+
# +where+, +find_by+, +and+, +or+, +nor+, +not+, +any_of+, and +none_of+.
|
|
310
|
+
# It also governs the string form of +where+, e.g.
|
|
311
|
+
# +where("this.name == 'admin'")+, which the server evaluates as +$where+.
|
|
312
|
+
# Applications relying on that form must set this option to true.
|
|
313
|
+
#
|
|
314
|
+
# The APIs that request JavaScript explicitly, +Criteria#for_js+ and
|
|
315
|
+
# +js_query+, are unaffected: there the developer has asked for it.
|
|
316
|
+
#
|
|
317
|
+
# Set to true to restore the unrestricted pass-through behavior.
|
|
318
|
+
#
|
|
319
|
+
# Note that this option is deliberately not tied to +load_defaults+: an
|
|
320
|
+
# application that has opted into older defaults still gets the guard, and
|
|
321
|
+
# must set this option explicitly to turn it off.
|
|
322
|
+
#
|
|
323
|
+
# See https://jira.mongodb.org/browse/MONGOID-5939,
|
|
324
|
+
# https://jira.mongodb.org/browse/MONGOID-5993,
|
|
325
|
+
# https://jira.mongodb.org/browse/MONGOID-5994 for details.
|
|
326
|
+
option :allow_unsafe_query_operators, default: false
|
|
327
|
+
|
|
328
|
+
# The maximum number of seconds that evaluating a single query in memory
|
|
329
|
+
# may spend executing regular expressions. Queries against an embedded
|
|
330
|
+
# association are evaluated in the calling thread, so a pattern built from
|
|
331
|
+
# user input runs locally and can otherwise consume unbounded CPU. The
|
|
332
|
+
# limit is cumulative over the whole query, since cost grows with the
|
|
333
|
+
# number of documents and conditions as well as with the pattern.
|
|
334
|
+
#
|
|
335
|
+
# Set to nil to remove the limit. On Ruby 3.2 and later the remaining
|
|
336
|
+
# budget is compiled into the pattern, so the limit counts only the time
|
|
337
|
+
# spent matching. Earlier Rubies have no per-Regexp timeout, so the query
|
|
338
|
+
# is bounded with Timeout instead and the limit is wall clock over the
|
|
339
|
+
# whole in-memory evaluation.
|
|
340
|
+
#
|
|
341
|
+
# See https://jira.mongodb.org/browse/MONGOID-5981 for details.
|
|
342
|
+
option :in_memory_regexp_time_limit, default: 5.0
|
|
343
|
+
|
|
297
344
|
# Returns the Config singleton, for use in the configure DSL.
|
|
298
345
|
#
|
|
299
346
|
# @return [ self ] The Config singleton.
|
|
@@ -539,6 +586,10 @@ module Mongoid
|
|
|
539
586
|
end
|
|
540
587
|
end
|
|
541
588
|
|
|
589
|
+
# Wraps configuration options that have been deprecated so that assigning
|
|
590
|
+
# them emits a deprecation warning. OPTIONS is intentionally empty when no
|
|
591
|
+
# options are currently deprecated; it is populated as options are retired,
|
|
592
|
+
# at which point this module rewrites their setters to warn.
|
|
542
593
|
module DeprecatedOptions
|
|
543
594
|
OPTIONS = %i[]
|
|
544
595
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'mongoid/field_readable'
|
|
4
|
+
|
|
3
5
|
module Mongoid
|
|
4
6
|
module Contextual
|
|
5
7
|
module Aggregable
|
|
6
8
|
# Contains behavior for aggregating values in memory.
|
|
7
9
|
module Memory
|
|
10
|
+
include FieldReadable
|
|
11
|
+
|
|
8
12
|
# Get all the aggregate values for the provided field.
|
|
9
13
|
# Provided for interface consistency with Aggregable::Mongo.
|
|
10
14
|
#
|
|
@@ -28,7 +32,7 @@ module Mongoid
|
|
|
28
32
|
#
|
|
29
33
|
# @return [ Numeric ] The average.
|
|
30
34
|
def avg(field)
|
|
31
|
-
total = count { |doc| !doc
|
|
35
|
+
total = count { |doc| !read_field_value(doc, field).nil? }
|
|
32
36
|
return nil unless total > 0
|
|
33
37
|
|
|
34
38
|
total = total.to_f if total.is_a?(Integer)
|
|
@@ -114,7 +118,7 @@ module Mongoid
|
|
|
114
118
|
def aggregate_by(field, method)
|
|
115
119
|
return nil unless any?
|
|
116
120
|
|
|
117
|
-
map { |doc| doc
|
|
121
|
+
map { |doc| read_field_value(doc, field) }.compact.public_send(method)
|
|
118
122
|
end
|
|
119
123
|
end
|
|
120
124
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'mongoid/contextual/aggregable/memory'
|
|
4
4
|
require 'mongoid/association/eager_loadable'
|
|
5
|
+
require 'mongoid/field_readable'
|
|
5
6
|
|
|
6
7
|
module Mongoid
|
|
7
8
|
module Contextual
|
|
@@ -15,6 +16,7 @@ module Mongoid
|
|
|
15
16
|
include Association::EagerLoadable
|
|
16
17
|
include Queryable
|
|
17
18
|
include Positional
|
|
19
|
+
include FieldReadable
|
|
18
20
|
|
|
19
21
|
# @attribute [r] root The root document.
|
|
20
22
|
# @attribute [r] path The atomic path.
|
|
@@ -174,10 +176,20 @@ module Mongoid
|
|
|
174
176
|
# @param [ Criteria ] criteria The criteria.
|
|
175
177
|
def initialize(criteria)
|
|
176
178
|
@criteria, @klass = criteria, criteria.klass
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
|
|
180
|
+
# Resolving the collection can build a Mongo::Client the first time it
|
|
181
|
+
# runs, so it happens before the budget opens. Where the budget is
|
|
182
|
+
# enforced with Timeout the exception is asynchronous, and landing in
|
|
183
|
+
# the middle of that would leave a half-built client behind.
|
|
184
|
+
if (first = criteria.documents.first)
|
|
185
|
+
@root = first._root
|
|
186
|
+
@collection = @root.collection
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# One regexp budget covers the whole scan, so that the limit bounds the
|
|
190
|
+
# query rather than each document individually.
|
|
191
|
+
@documents = Matcher::RegexpBudget.open(criteria.selector) do
|
|
192
|
+
criteria.documents.select { |doc| doc._matches?(criteria.selector) }
|
|
181
193
|
end
|
|
182
194
|
apply_sorting
|
|
183
195
|
apply_options
|
|
@@ -739,10 +751,9 @@ module Mongoid
|
|
|
739
751
|
# If this is a localized field, and there are remaining, get the
|
|
740
752
|
# _translations hash so that we can get the specified translation in
|
|
741
753
|
# the remaining
|
|
742
|
-
document.
|
|
754
|
+
document.public_send("#{segment}_translations") if field&.localized?
|
|
743
755
|
end
|
|
744
|
-
|
|
745
|
-
res.nil? ? document.try(meth) : res
|
|
756
|
+
res.nil? ? read_field_value(document, segment) : res
|
|
746
757
|
elsif document.is_a?(Hash)
|
|
747
758
|
# TODO: Remove the indifferent access when implementing MONGOID-5410.
|
|
748
759
|
if document.key?(segment.to_s)
|
|
@@ -44,11 +44,13 @@ module Mongoid
|
|
|
44
44
|
clone
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Returns whether to use $lookup aggregation for eager loading.
|
|
47
|
+
# Returns whether to use $lookup aggregation for eager loading. Only when
|
|
48
|
+
# eager_load was requested and there is something to load: an empty
|
|
49
|
+
# inclusion list (e.g. eager_load([])) falls back to the normal path.
|
|
48
50
|
#
|
|
49
51
|
# @return [ true | false ] Whether to use $lookup.
|
|
50
52
|
def use_lookup?
|
|
51
|
-
!!@use_lookup
|
|
53
|
+
!!@use_lookup && inclusions.any?
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# Get a list of criteria that are to be executed for eager loading.
|
|
@@ -96,20 +98,42 @@ module Mongoid
|
|
|
96
98
|
# The names of the association(s) to eager load.
|
|
97
99
|
def extract_includes_list(_parent_class, parent, is_eager_load = false, *relations_list)
|
|
98
100
|
relations_list.flatten.each do |relation_object|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
# Normalize a bare association name to a hash with no nested
|
|
102
|
+
# inclusions, so both forms share one resolution path below.
|
|
103
|
+
relations = relation_object.is_a?(Hash) ? relation_object : { relation_object => nil }
|
|
104
|
+
|
|
105
|
+
relations.each do |relation, nested|
|
|
106
|
+
associations = resolve_inclusion_associations(_parent_class, relation, is_eager_load)
|
|
107
|
+
raise_eager_error(is_eager_load, _parent_class, relation) if associations.empty?
|
|
108
|
+
|
|
109
|
+
associations.each do |association|
|
|
103
110
|
add_inclusion(association, parent)
|
|
104
|
-
extract_includes_list(association.klass, association.name, is_eager_load,
|
|
111
|
+
extract_includes_list(association.klass, association.name, is_eager_load, nested) if nested
|
|
105
112
|
end
|
|
106
|
-
else
|
|
107
|
-
association = _parent_class.reflect_on_association(relation_object)
|
|
108
|
-
raise_eager_error(is_eager_load, _parent_class, relation_object) unless association
|
|
109
|
-
add_inclusion(association, parent)
|
|
110
113
|
end
|
|
111
114
|
end
|
|
112
115
|
end
|
|
116
|
+
|
|
117
|
+
# Resolve the association(s) matching the given relation name. For the
|
|
118
|
+
# regular #includes path, only the parent class is consulted. For the
|
|
119
|
+
# #eager_load ($lookup) path, its subclasses are consulted as well, so
|
|
120
|
+
# associations defined only on a subclass can also be eager-loaded when
|
|
121
|
+
# querying through the superclass.
|
|
122
|
+
#
|
|
123
|
+
# @param [ Class ] parent_class The class to start the lookup from.
|
|
124
|
+
# @param [ Symbol | String ] relation The association name.
|
|
125
|
+
# @param [ Boolean ] is_eager_load Whether to consider subclasses.
|
|
126
|
+
#
|
|
127
|
+
# @return [ Array<Mongoid::Association::Relatable> ] Matching associations.
|
|
128
|
+
def resolve_inclusion_associations(parent_class, relation, is_eager_load)
|
|
129
|
+
if association = parent_class.reflect_on_association(relation)
|
|
130
|
+
return [ association ]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return [] unless is_eager_load
|
|
134
|
+
|
|
135
|
+
parent_class.descendants.filter_map { |sub| sub.reflect_on_association(relation) }
|
|
136
|
+
end
|
|
113
137
|
end
|
|
114
138
|
|
|
115
139
|
def raise_eager_error(is_eager_load, klass, relation)
|
|
@@ -320,6 +320,10 @@ module Mongoid
|
|
|
320
320
|
end
|
|
321
321
|
end
|
|
322
322
|
end
|
|
323
|
+
# Every query method that takes a user-supplied expression normalizes
|
|
324
|
+
# it here, so this is where the operator guard is enforced. See
|
|
325
|
+
# Selectable#_mongoid_validate_operators! for what it does not cover.
|
|
326
|
+
_mongoid_validate_operators!(result)
|
|
323
327
|
result
|
|
324
328
|
end
|
|
325
329
|
|
|
@@ -491,16 +491,16 @@ module Mongoid
|
|
|
491
491
|
!!negating
|
|
492
492
|
end
|
|
493
493
|
|
|
494
|
-
# Negate the arguments
|
|
495
|
-
#
|
|
496
|
-
# @example Negate the next selection.
|
|
497
|
-
# selectable.not.in(field: [ 1, 2 ])
|
|
494
|
+
# Negate the arguments.
|
|
498
495
|
#
|
|
499
496
|
# @example Add the $not criterion.
|
|
500
|
-
#
|
|
497
|
+
# Person.not(name: /Bob/)
|
|
501
498
|
#
|
|
502
499
|
# @example Execute a $not in a where query.
|
|
503
|
-
#
|
|
500
|
+
# Person.where(:name.not => /Bob/)
|
|
501
|
+
#
|
|
502
|
+
# @example Negate a Criteria instance
|
|
503
|
+
# Person.not(Person.where(...))
|
|
504
504
|
#
|
|
505
505
|
# @param [ [ Hash | Criteria ]... ] *criteria The key/value pair
|
|
506
506
|
# matches or Criteria objects to negate.
|
|
@@ -508,6 +508,14 @@ module Mongoid
|
|
|
508
508
|
# @return [ Selectable ] The new selectable.
|
|
509
509
|
def not(*criteria)
|
|
510
510
|
if criteria.empty?
|
|
511
|
+
# @deprecated
|
|
512
|
+
Mongoid.deprecation_warning(
|
|
513
|
+
:not_sans_arguments,
|
|
514
|
+
'Calling `#not` without arguments is deprecated, and will be ' \
|
|
515
|
+
'removed in the next major version. Instead, pass the Hash or ' \
|
|
516
|
+
'Criteria instance to negate.',
|
|
517
|
+
caller_locations
|
|
518
|
+
)
|
|
511
519
|
dup.tap { |query| query.negating = !query.negating }
|
|
512
520
|
else
|
|
513
521
|
criteria.compact.inject(clone) do |c, new_s|
|
|
@@ -768,6 +776,12 @@ module Mongoid
|
|
|
768
776
|
# only ever specify one criterion to #where.
|
|
769
777
|
@criterion = criterion
|
|
770
778
|
if criterion.is_a?(String)
|
|
779
|
+
unless Mongoid.allow_unsafe_query_operators?
|
|
780
|
+
raise Errors::InvalidQuery,
|
|
781
|
+
"String criteria are not allowed because they compile to the '$where' operator, " \
|
|
782
|
+
'which is not allowed in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
|
|
783
|
+
'to permit all operators.'
|
|
784
|
+
end
|
|
771
785
|
query.js_query(criterion)
|
|
772
786
|
else
|
|
773
787
|
query.expr_query(criterion)
|
|
@@ -775,6 +789,19 @@ module Mongoid
|
|
|
775
789
|
end
|
|
776
790
|
end
|
|
777
791
|
|
|
792
|
+
# Operators permitted at the top level of a query expression without
|
|
793
|
+
# opt-in. Excludes $where (JS execution) and other operators not needed
|
|
794
|
+
# for ordinary application queries.
|
|
795
|
+
ALLOWED_QUERY_OPERATORS = %w[
|
|
796
|
+
$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue
|
|
797
|
+
].freeze
|
|
798
|
+
|
|
799
|
+
# Operators that execute server-side JavaScript. These are rejected at
|
|
800
|
+
# any depth, not just at the top level: the allowlist above permits
|
|
801
|
+
# $expr and the logical operators, and their values are arbitrary
|
|
802
|
+
# nested expressions that can carry $function or $where.
|
|
803
|
+
JAVASCRIPT_QUERY_OPERATORS = %w[$where $function $accumulator].freeze
|
|
804
|
+
|
|
778
805
|
# Adds the specified expression to the query.
|
|
779
806
|
#
|
|
780
807
|
# Criterion must be a hash in one of the following forms:
|
|
@@ -798,12 +825,13 @@ module Mongoid
|
|
|
798
825
|
raise Errors::InvalidQuery, "Expression must be a Hash: #{Errors::InvalidQuery.truncate_expr(criterion)}"
|
|
799
826
|
end
|
|
800
827
|
|
|
828
|
+
# The operator guard is applied by _mongoid_expand_keys, which every
|
|
829
|
+
# query method that accepts a user-supplied expression passes through.
|
|
801
830
|
normalized = _mongoid_expand_keys(criterion)
|
|
802
831
|
clone.tap do |query|
|
|
803
832
|
normalized.each do |field, value|
|
|
804
833
|
field_s = field.to_s
|
|
805
834
|
if field_s.start_with?('$')
|
|
806
|
-
# Query expression-level operator, like $and or $where
|
|
807
835
|
query.add_operator_expression(field_s, value)
|
|
808
836
|
else
|
|
809
837
|
query.add_field_expression(field, value)
|
|
@@ -837,6 +865,94 @@ module Mongoid
|
|
|
837
865
|
|
|
838
866
|
private
|
|
839
867
|
|
|
868
|
+
# Enforces the operator rules governed by the
|
|
869
|
+
# +allow_unsafe_query_operators+ configuration option against a
|
|
870
|
+
# normalized query expression.
|
|
871
|
+
#
|
|
872
|
+
# Two rules apply, and both are skipped when the option is true:
|
|
873
|
+
#
|
|
874
|
+
# - An operator at the top level of the expression must appear in
|
|
875
|
+
# ALLOWED_QUERY_OPERATORS.
|
|
876
|
+
# - An operator in JAVASCRIPT_QUERY_OPERATORS is rejected at any depth.
|
|
877
|
+
#
|
|
878
|
+
# This is called from #_mongoid_expand_keys rather than from the
|
|
879
|
+
# individual query methods, because that is the one point every query
|
|
880
|
+
# method taking a user-supplied expression passes through on its way to
|
|
881
|
+
# the selector.
|
|
882
|
+
#
|
|
883
|
+
# It deliberately does not cover the APIs that ask for JavaScript
|
|
884
|
+
# outright, such as #js_query and Criteria#for_js: there the developer
|
|
885
|
+
# has chosen server-side JavaScript, so there is nothing to guard
|
|
886
|
+
# against. The same goes for the low-level Storable methods
|
|
887
|
+
# (#add_field_expression, #add_operator_expression), which write to the
|
|
888
|
+
# selector directly.
|
|
889
|
+
#
|
|
890
|
+
# @param [ Hash ] expr A normalized query expression.
|
|
891
|
+
#
|
|
892
|
+
# @raise [ Errors::InvalidQuery ] If a disallowed operator is present.
|
|
893
|
+
#
|
|
894
|
+
# @api private
|
|
895
|
+
def _mongoid_validate_operators!(expr)
|
|
896
|
+
return if Mongoid.allow_unsafe_query_operators?
|
|
897
|
+
|
|
898
|
+
expr.each_key do |field|
|
|
899
|
+
field_s = field.to_s
|
|
900
|
+
next unless field_s.start_with?('$')
|
|
901
|
+
next if ALLOWED_QUERY_OPERATORS.include?(field_s)
|
|
902
|
+
|
|
903
|
+
raise Errors::InvalidQuery,
|
|
904
|
+
"Operator '#{field_s}' is not allowed in a query expression. " \
|
|
905
|
+
'Set Mongoid.allow_unsafe_query_operators = true to permit all operators.'
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
_mongoid_validate_no_javascript!(expr)
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# Walks a query expression looking for operators that execute
|
|
912
|
+
# server-side JavaScript, descending through both hashes and arrays so
|
|
913
|
+
# that nested forms such as {'$expr' => {'$function' => ...}} and
|
|
914
|
+
# {'$or' => [ {'$where' => ...} ]} are caught.
|
|
915
|
+
#
|
|
916
|
+
# The walk does not distinguish operator position from value position,
|
|
917
|
+
# so it also rejects queries where a JavaScript operator name appears as
|
|
918
|
+
# data rather than as an operator. Server 5.0+ permits $-prefixed field
|
|
919
|
+
# names in stored documents, which makes this reachable:
|
|
920
|
+
#
|
|
921
|
+
# Doc.where(payload: { '$eq' => { '$function' => 'abc' } })
|
|
922
|
+
#
|
|
923
|
+
# Here the $eq marks its argument as a literal value to compare, so the
|
|
924
|
+
# server never evaluates it, but the guard raises anyway. The only
|
|
925
|
+
# workaround today is the global allow_unsafe_query_operators flag.
|
|
926
|
+
#
|
|
927
|
+
# TODO: discuss whether to track operator position (skipping the subtree
|
|
928
|
+
# under $eq, $ne, $in, $nin, and $elemMatch values) in a future
|
|
929
|
+
# iteration. It removes the false positive but adds exactly the kind of
|
|
930
|
+
# state that a real bypass could hide in, so it was left out for now.
|
|
931
|
+
#
|
|
932
|
+
# @param [ Object ] object A fragment of a query expression.
|
|
933
|
+
#
|
|
934
|
+
# @raise [ Errors::InvalidQuery ] If a JavaScript operator is present.
|
|
935
|
+
#
|
|
936
|
+
# @api private
|
|
937
|
+
def _mongoid_validate_no_javascript!(object)
|
|
938
|
+
case object
|
|
939
|
+
when Hash
|
|
940
|
+
object.each do |key, value|
|
|
941
|
+
key_s = key.to_s
|
|
942
|
+
if JAVASCRIPT_QUERY_OPERATORS.include?(key_s)
|
|
943
|
+
raise Errors::InvalidQuery,
|
|
944
|
+
"Operator '#{key_s}' executes server-side JavaScript and is not allowed " \
|
|
945
|
+
'anywhere in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
|
|
946
|
+
'to permit all operators.'
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
_mongoid_validate_no_javascript!(value)
|
|
950
|
+
end
|
|
951
|
+
when Array
|
|
952
|
+
object.each { |value| _mongoid_validate_no_javascript!(value) }
|
|
953
|
+
end
|
|
954
|
+
end
|
|
955
|
+
|
|
840
956
|
# Force the values of the criterion to be evolved.
|
|
841
957
|
#
|
|
842
958
|
# @api private
|
data/lib/mongoid/deprecable.rb
CHANGED
|
@@ -7,6 +7,29 @@ module Mongoid
|
|
|
7
7
|
#
|
|
8
8
|
# @api private
|
|
9
9
|
module Deprecable
|
|
10
|
+
# A Mongoid::Deprecation instance to use for reporting deprecations
|
|
11
|
+
def deprecator
|
|
12
|
+
@deprecator ||= Mongoid::Deprecation.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Resets all deprecation warnings. For use in tests.
|
|
16
|
+
def reset_deprecation_warnings!
|
|
17
|
+
DEPRECATION_WARNING_MUTEX.synchronize { @deprecation_warnings = {} }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Emits a warning using the current deprecator. If the given warning (as
|
|
21
|
+
# identified by `id`) has already been issued previously, this is a no-op.
|
|
22
|
+
#
|
|
23
|
+
# @param [ Symbol ] id The unique identifier for this warning.
|
|
24
|
+
# @param [ String ] warning The warning message to emit.
|
|
25
|
+
# @param [ Array<Thread::Backtrace::Location> | nil ] callstack The backtrace at the call site.
|
|
26
|
+
def deprecation_warning(id, warning, callstack = nil)
|
|
27
|
+
site = callstack&.first
|
|
28
|
+
deprecation_warning_guard(id, site ? "#{site.path}:#{site.lineno}" : nil) do
|
|
29
|
+
deprecator.warn(warning, callstack)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
10
33
|
# Declares method(s) as deprecated.
|
|
11
34
|
#
|
|
12
35
|
# @example Deprecate a method.
|
|
@@ -25,8 +48,28 @@ module Mongoid
|
|
|
25
48
|
# @param [ [ Symbol | Hash<Symbol, [ Symbol | String ]> ]... ] *method_descriptors
|
|
26
49
|
# The methods to deprecate, with optional replacement instructions.
|
|
27
50
|
def deprecate(target_module, *method_descriptors)
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
deprecator.deprecate_methods(target_module, *method_descriptors)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# The Mutex instance used to guard the deprecation warning flags.
|
|
57
|
+
DEPRECATION_WARNING_MUTEX = Mutex.new
|
|
58
|
+
|
|
59
|
+
# Wraps access to the warnings Hash in a synchronize block. If the given
|
|
60
|
+
# id+callsite has not been warned already, the method will yield to a block and then
|
|
61
|
+
# flag the id. Otherwise, it returns immediately.
|
|
62
|
+
def deprecation_warning_guard(id, callsite)
|
|
63
|
+
DEPRECATION_WARNING_MUTEX.synchronize do
|
|
64
|
+
@deprecation_warnings ||= {}
|
|
65
|
+
|
|
66
|
+
key = "#{id}:#{callsite}"
|
|
67
|
+
return if @deprecation_warnings.key?(key)
|
|
68
|
+
|
|
69
|
+
yield
|
|
70
|
+
|
|
71
|
+
@deprecation_warnings[key] = true
|
|
72
|
+
end
|
|
30
73
|
end
|
|
31
74
|
end
|
|
32
75
|
end
|
data/lib/mongoid/encryptable.rb
CHANGED
|
@@ -36,6 +36,52 @@ module Mongoid
|
|
|
36
36
|
!encrypt_metadata.empty? || fields.any? { |_, field| field.is_a?(Mongoid::Fields::Encrypted) }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Whether an encryption schema has to be generated for this model.
|
|
40
|
+
#
|
|
41
|
+
# True when the model declares encryption itself, and also when any model
|
|
42
|
+
# reachable through its embeds_one relations does. A model in the second
|
|
43
|
+
# group has no encrypted field of its own, but its collection still needs
|
|
44
|
+
# a schema, otherwise the embedded fields are written in plaintext.
|
|
45
|
+
#
|
|
46
|
+
# The answer is memoized, since this runs on the persistence path.
|
|
47
|
+
# Declaring encryption on a model after it has already been persisted is
|
|
48
|
+
# not supported.
|
|
49
|
+
#
|
|
50
|
+
# @return [ true | false ] Whether the model needs an encryption schema.
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
53
|
+
def requires_encryption_schema?
|
|
54
|
+
return @requires_encryption_schema if defined?(@requires_encryption_schema)
|
|
55
|
+
|
|
56
|
+
@requires_encryption_schema = encrypted? || embeds_encrypted?([ self ])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether any model reachable through this model's embeds_one relations
|
|
60
|
+
# declares encryption.
|
|
61
|
+
#
|
|
62
|
+
# embeds_many is not considered: libmongocrypt cannot express per-field
|
|
63
|
+
# encryption under array items, so those fields are never mapped.
|
|
64
|
+
#
|
|
65
|
+
# @param [ Array<Class> ] path The models the walk is already inside of.
|
|
66
|
+
# A model embedding itself terminates here.
|
|
67
|
+
#
|
|
68
|
+
# @return [ true | false ] Whether an embedded model is encrypted.
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
def embeds_encrypted?(path)
|
|
72
|
+
relations.each_value.any? do |relation|
|
|
73
|
+
next false unless relation.is_a?(Association::Embedded::EmbedsOne)
|
|
74
|
+
|
|
75
|
+
klass = relation.try_relation_class
|
|
76
|
+
# An association target does not have to be a Mongoid document, and
|
|
77
|
+
# the class it names does not have to exist.
|
|
78
|
+
next false unless klass.respond_to?(:encrypted?)
|
|
79
|
+
next false if path.include?(klass)
|
|
80
|
+
|
|
81
|
+
klass.encrypted? || klass.embeds_encrypted?(path + [ klass ])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
39
85
|
# Override the key_id for the model.
|
|
40
86
|
#
|
|
41
87
|
# This method is solely for testing purposes and should not be used in
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Errors
|
|
5
|
+
# Redacts credentials from a client configuration hash before it is
|
|
6
|
+
# interpolated into an exception message.
|
|
7
|
+
module ConfigRedactor
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
REDACTED = '[REDACTED]'
|
|
11
|
+
|
|
12
|
+
# Top-level keys whose values should be replaced wholesale.
|
|
13
|
+
SENSITIVE_KEYS = %w[password auto_encryption_options].freeze
|
|
14
|
+
|
|
15
|
+
# Match the userinfo portion of a MongoDB connection string.
|
|
16
|
+
URI_USERINFO = %r{\A(mongodb(?:\+srv)?://)[^@/]+@}.freeze
|
|
17
|
+
|
|
18
|
+
# Return a copy of the given config hash with sensitive values redacted.
|
|
19
|
+
# Recurses into nested hashes so that, e.g., `:options =>
|
|
20
|
+
# { :auto_encryption_options => ... }` is also covered. Non-hash inputs
|
|
21
|
+
# are returned unchanged.
|
|
22
|
+
def redact(config)
|
|
23
|
+
return config unless config.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
config.each_with_object({}) do |(key, value), result|
|
|
26
|
+
result[key] = redact_value(key, value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def redact_value(key, value)
|
|
31
|
+
if SENSITIVE_KEYS.include?(key.to_s)
|
|
32
|
+
REDACTED
|
|
33
|
+
elsif key.to_s == 'uri' && value.is_a?(String)
|
|
34
|
+
value.sub(URI_USERINFO, "\\1#{REDACTED}@")
|
|
35
|
+
elsif value.is_a?(Hash)
|
|
36
|
+
redact(value)
|
|
37
|
+
else
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|