mongoid 8.1.11 → 8.1.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 +26 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/embedded/batchable.rb +11 -10
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +61 -0
- data/lib/mongoid/association/nested/many.rb +38 -3
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/clients/factory.rb +4 -0
- data/lib/mongoid/config.rb +62 -0
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +12 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/extensions/hash.rb +27 -1
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +3 -5
- data/lib/mongoid/matcher/regex.rb +8 -9
- data/lib/mongoid/matcher/regexp_budget.rb +389 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/threaded.rb +3 -0
- data/lib/mongoid/touchable.rb +1 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +1 -0
- data/spec/integration/app_spec.rb +8 -0
- data/spec/integration/associations/embeds_many_spec.rb +41 -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/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 +30 -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 +235 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/extensions/hash_spec.rb +236 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/touchable_spec.rb +12 -0
- data/spec/shared/CANDIDATE.md +28 -0
- data/spec/shared/lib/mrss/spec_organizer.rb +32 -3
- data/spec/shared/shlib/server.sh +1 -1
- metadata +13 -2
|
@@ -822,6 +822,12 @@ module Mongoid
|
|
|
822
822
|
# only ever specify one criterion to #where.
|
|
823
823
|
@criterion = criterion
|
|
824
824
|
if criterion.is_a?(String)
|
|
825
|
+
unless Mongoid.allow_unsafe_query_operators?
|
|
826
|
+
raise Errors::InvalidQuery,
|
|
827
|
+
"String criteria are not allowed because they compile to the '$where' operator, " \
|
|
828
|
+
'which is not allowed in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
|
|
829
|
+
'to permit all operators.'
|
|
830
|
+
end
|
|
825
831
|
js_query(criterion)
|
|
826
832
|
else
|
|
827
833
|
expr_query(criterion)
|
|
@@ -831,6 +837,19 @@ module Mongoid
|
|
|
831
837
|
|
|
832
838
|
private
|
|
833
839
|
|
|
840
|
+
# Operators permitted at the top level of a query expression without
|
|
841
|
+
# opt-in. Excludes $where (JS execution) and other operators not needed
|
|
842
|
+
# for ordinary application queries.
|
|
843
|
+
ALLOWED_QUERY_OPERATORS = %w[
|
|
844
|
+
$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue
|
|
845
|
+
].freeze
|
|
846
|
+
|
|
847
|
+
# Operators that execute server-side JavaScript. These are rejected at
|
|
848
|
+
# any depth, not just at the top level: the allowlist above permits
|
|
849
|
+
# $expr and the logical operators, and their values are arbitrary
|
|
850
|
+
# nested expressions that can carry $function or $where.
|
|
851
|
+
JAVASCRIPT_QUERY_OPERATORS = %w[$where $function $accumulator].freeze
|
|
852
|
+
|
|
834
853
|
# Adds the specified expression to the query.
|
|
835
854
|
#
|
|
836
855
|
# Criterion must be a hash in one of the following forms:
|
|
@@ -856,6 +875,8 @@ module Mongoid
|
|
|
856
875
|
raise Errors::InvalidQuery, "Expression must be a Hash: #{Errors::InvalidQuery.truncate_expr(criterion)}"
|
|
857
876
|
end
|
|
858
877
|
|
|
878
|
+
# The operator guard is applied by _mongoid_expand_keys, which every
|
|
879
|
+
# query method that accepts a user-supplied expression passes through.
|
|
859
880
|
normalized = _mongoid_expand_keys(criterion)
|
|
860
881
|
clone.tap do |query|
|
|
861
882
|
normalized.each do |field, value|
|
|
@@ -871,6 +892,94 @@ module Mongoid
|
|
|
871
892
|
end
|
|
872
893
|
end
|
|
873
894
|
|
|
895
|
+
# Enforces the operator rules governed by the
|
|
896
|
+
# +allow_unsafe_query_operators+ configuration option against a
|
|
897
|
+
# normalized query expression.
|
|
898
|
+
#
|
|
899
|
+
# Two rules apply, and both are skipped when the option is true:
|
|
900
|
+
#
|
|
901
|
+
# - An operator at the top level of the expression must appear in
|
|
902
|
+
# ALLOWED_QUERY_OPERATORS.
|
|
903
|
+
# - An operator in JAVASCRIPT_QUERY_OPERATORS is rejected at any depth.
|
|
904
|
+
#
|
|
905
|
+
# This is called from #_mongoid_expand_keys rather than from the
|
|
906
|
+
# individual query methods, because that is the one point every query
|
|
907
|
+
# method taking a user-supplied expression passes through on its way to
|
|
908
|
+
# the selector.
|
|
909
|
+
#
|
|
910
|
+
# It deliberately does not cover the APIs that ask for JavaScript
|
|
911
|
+
# outright, such as #js_query and Criteria#for_js: there the developer
|
|
912
|
+
# has chosen server-side JavaScript, so there is nothing to guard
|
|
913
|
+
# against. The same goes for the low-level Storable methods
|
|
914
|
+
# (#add_field_expression, #add_operator_expression), which write to the
|
|
915
|
+
# selector directly.
|
|
916
|
+
#
|
|
917
|
+
# @param [ Hash ] expr A normalized query expression.
|
|
918
|
+
#
|
|
919
|
+
# @raise [ Errors::InvalidQuery ] If a disallowed operator is present.
|
|
920
|
+
#
|
|
921
|
+
# @api private
|
|
922
|
+
def _mongoid_validate_operators!(expr)
|
|
923
|
+
return if Mongoid.allow_unsafe_query_operators?
|
|
924
|
+
|
|
925
|
+
expr.each_key do |field|
|
|
926
|
+
field_s = field.to_s
|
|
927
|
+
next unless field_s.start_with?('$')
|
|
928
|
+
next if ALLOWED_QUERY_OPERATORS.include?(field_s)
|
|
929
|
+
|
|
930
|
+
raise Errors::InvalidQuery,
|
|
931
|
+
"Operator '#{field_s}' is not allowed in a query expression. " \
|
|
932
|
+
'Set Mongoid.allow_unsafe_query_operators = true to permit all operators.'
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
_mongoid_validate_no_javascript!(expr)
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Walks a query expression looking for operators that execute
|
|
939
|
+
# server-side JavaScript, descending through both hashes and arrays so
|
|
940
|
+
# that nested forms such as {'$expr' => {'$function' => ...}} and
|
|
941
|
+
# {'$or' => [ {'$where' => ...} ]} are caught.
|
|
942
|
+
#
|
|
943
|
+
# The walk does not distinguish operator position from value position,
|
|
944
|
+
# so it also rejects queries where a JavaScript operator name appears as
|
|
945
|
+
# data rather than as an operator. Server 5.0+ permits $-prefixed field
|
|
946
|
+
# names in stored documents, which makes this reachable:
|
|
947
|
+
#
|
|
948
|
+
# Doc.where(payload: { '$eq' => { '$function' => 'abc' } })
|
|
949
|
+
#
|
|
950
|
+
# Here the $eq marks its argument as a literal value to compare, so the
|
|
951
|
+
# server never evaluates it, but the guard raises anyway. The only
|
|
952
|
+
# workaround today is the global allow_unsafe_query_operators flag.
|
|
953
|
+
#
|
|
954
|
+
# TODO: discuss whether to track operator position (skipping the subtree
|
|
955
|
+
# under $eq, $ne, $in, $nin, and $elemMatch values) in a future
|
|
956
|
+
# iteration. It removes the false positive but adds exactly the kind of
|
|
957
|
+
# state that a real bypass could hide in, so it was left out for now.
|
|
958
|
+
#
|
|
959
|
+
# @param [ Object ] object A fragment of a query expression.
|
|
960
|
+
#
|
|
961
|
+
# @raise [ Errors::InvalidQuery ] If a JavaScript operator is present.
|
|
962
|
+
#
|
|
963
|
+
# @api private
|
|
964
|
+
def _mongoid_validate_no_javascript!(object)
|
|
965
|
+
case object
|
|
966
|
+
when Hash
|
|
967
|
+
object.each do |key, value|
|
|
968
|
+
key_s = key.to_s
|
|
969
|
+
if JAVASCRIPT_QUERY_OPERATORS.include?(key_s)
|
|
970
|
+
raise Errors::InvalidQuery,
|
|
971
|
+
"Operator '#{key_s}' executes server-side JavaScript and is not allowed " \
|
|
972
|
+
'anywhere in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
|
|
973
|
+
'to permit all operators.'
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
_mongoid_validate_no_javascript!(value)
|
|
977
|
+
end
|
|
978
|
+
when Array
|
|
979
|
+
object.each { |value| _mongoid_validate_no_javascript!(value) }
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
|
|
874
983
|
# Force the values of the criterion to be evolved.
|
|
875
984
|
#
|
|
876
985
|
# @api private
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Errors
|
|
5
|
+
# This error is raised when evaluating a query in memory exceeds
|
|
6
|
+
# Mongoid::Config.in_memory_regexp_time_limit.
|
|
7
|
+
#
|
|
8
|
+
# What the limit bounds depends on the Ruby in use. Where per-Regexp
|
|
9
|
+
# timeouts are available it is the time spent executing regular
|
|
10
|
+
# expressions; elsewhere it is the elapsed time of the whole in-memory
|
|
11
|
+
# evaluation. The message is worded to hold either way.
|
|
12
|
+
class InMemoryRegexpTimeout < MongoidError
|
|
13
|
+
# Create the new error.
|
|
14
|
+
#
|
|
15
|
+
# @example Create the new in-memory regexp timeout error.
|
|
16
|
+
# InMemoryRegexpTimeout.new(5.0)
|
|
17
|
+
#
|
|
18
|
+
# @param [ Float ] limit The limit that was exceeded, in seconds. Not
|
|
19
|
+
# always the configured one: a global Regexp.timeout stricter than the
|
|
20
|
+
# configuration takes its place.
|
|
21
|
+
def initialize(limit)
|
|
22
|
+
super(compose_message('in_memory_regexp_timeout', limit: limit))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/mongoid/errors.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "mongoid/errors/document_not_found"
|
|
|
10
10
|
require "mongoid/errors/empty_config_file"
|
|
11
11
|
require "mongoid/errors/immutable_attribute"
|
|
12
12
|
require "mongoid/errors/in_memory_collation_not_supported"
|
|
13
|
+
require "mongoid/errors/in_memory_regexp_timeout"
|
|
13
14
|
require "mongoid/errors/invalid_async_query_executor"
|
|
14
15
|
require "mongoid/errors/invalid_collection"
|
|
15
16
|
require "mongoid/errors/invalid_config_file"
|
|
@@ -167,6 +167,28 @@ module Mongoid
|
|
|
167
167
|
true
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
ALLOWED_TO_CRITERIA_METHODS = %i[
|
|
171
|
+
all all_in all_of and any_in any_of asc ascending
|
|
172
|
+
batch_size between
|
|
173
|
+
collation comment cursor_type
|
|
174
|
+
desc descending
|
|
175
|
+
elem_match eq exists extras
|
|
176
|
+
geo_spatial group gt gte
|
|
177
|
+
hint
|
|
178
|
+
in includes
|
|
179
|
+
limit lt lte
|
|
180
|
+
max_distance max_scan max_time_ms merge mod
|
|
181
|
+
ne near near_sphere nin no_timeout none none_of nor not not_in
|
|
182
|
+
offset only or order order_by
|
|
183
|
+
project
|
|
184
|
+
raw read reorder
|
|
185
|
+
scoped skip slice snapshot
|
|
186
|
+
text_search type
|
|
187
|
+
unscoped unwind
|
|
188
|
+
where with_size with_type without
|
|
189
|
+
].freeze
|
|
190
|
+
private_constant :ALLOWED_TO_CRITERIA_METHODS
|
|
191
|
+
|
|
170
192
|
# Convert this hash to a criteria. Will iterate over each keys in the
|
|
171
193
|
# hash which must correspond to method on a criteria object. The hash
|
|
172
194
|
# must also include a "klass" key.
|
|
@@ -178,7 +200,11 @@ module Mongoid
|
|
|
178
200
|
def to_criteria
|
|
179
201
|
criteria = Criteria.new(delete(:klass) || delete("klass"))
|
|
180
202
|
each_pair do |method, args|
|
|
181
|
-
|
|
203
|
+
method_sym = method.to_sym
|
|
204
|
+
unless ALLOWED_TO_CRITERIA_METHODS.include?(method_sym)
|
|
205
|
+
raise ArgumentError, "Method '#{method}' is not allowed in to_criteria"
|
|
206
|
+
end
|
|
207
|
+
criteria = criteria.public_send(method_sym, args)
|
|
182
208
|
end
|
|
183
209
|
criteria
|
|
184
210
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rubocop:todo all
|
|
3
|
+
|
|
4
|
+
module Mongoid
|
|
5
|
+
# Reads the value of a field name from a document without dispatching the
|
|
6
|
+
# name as an arbitrary method.
|
|
7
|
+
#
|
|
8
|
+
# Field names may come from application input, where a name like +destroy+
|
|
9
|
+
# or +attributes+ would delete the document or disclose its contents.
|
|
10
|
+
# Declared field and association names are validated when they are declared
|
|
11
|
+
# (see Mongoid.destructive_fields), so a name that resolves to one of them
|
|
12
|
+
# is safe to send to a document. Any other name is read from the attributes
|
|
13
|
+
# hash, which is what the database-backed query contexts do.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module FieldReadable
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# Read the value of the given field name from the given document.
|
|
20
|
+
#
|
|
21
|
+
# @param [ Document ] document The document to read from.
|
|
22
|
+
# @param [ String | Symbol ] name The name of the field.
|
|
23
|
+
#
|
|
24
|
+
# @return [ Object | nil ] The value of the field, or nil when the name
|
|
25
|
+
# is neither a declared field nor present in the attributes.
|
|
26
|
+
def read_field_value(document, name)
|
|
27
|
+
name = name.to_s
|
|
28
|
+
# A blank name cannot name a field. Return nil, which is what reading
|
|
29
|
+
# one has always done.
|
|
30
|
+
return nil if name.blank?
|
|
31
|
+
|
|
32
|
+
if (meth = readable_method_for(document.class, name))
|
|
33
|
+
document.public_send(meth)
|
|
34
|
+
else
|
|
35
|
+
document.attributes[document.class.database_field_name(name)]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Resolve the given name to a method that is declared by the given class,
|
|
40
|
+
# and therefore safe to send to one of its instances.
|
|
41
|
+
#
|
|
42
|
+
# @param [ Class ] klass The document class.
|
|
43
|
+
# @param [ String ] name The name of the field.
|
|
44
|
+
#
|
|
45
|
+
# @return [ String | nil ] The method to send, or nil when the name is
|
|
46
|
+
# not declared by the class.
|
|
47
|
+
def readable_method_for(klass, name)
|
|
48
|
+
# Fields, associations, and field aliases each define a reader of their
|
|
49
|
+
# own name. Note that associations must be resolved before aliases: a
|
|
50
|
+
# belongs_to aliases its own name to its foreign key, and reading
|
|
51
|
+
# `band` must give the document, not the id.
|
|
52
|
+
return name if klass.relations.key?(name) ||
|
|
53
|
+
klass.fields.key?(name) ||
|
|
54
|
+
klass.aliased_fields.key?(name)
|
|
55
|
+
|
|
56
|
+
# An association may also be named by its `store_as`, which has no
|
|
57
|
+
# reader of its own, or by its ids accessor, which does.
|
|
58
|
+
if (assoc = klass.relations[klass.aliased_associations[name]])
|
|
59
|
+
return (assoc.store_as == name) ? assoc.name.to_s : name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Localized fields also get a _translations reader, which does not
|
|
63
|
+
# appear in the fields hash.
|
|
64
|
+
base = name.delete_suffix(Fields::TRANSLATIONS_SFX)
|
|
65
|
+
return nil if base == name
|
|
66
|
+
|
|
67
|
+
name if klass.fields[klass.database_field_name(base)]&.localized?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/mongoid/matchable.rb
CHANGED
|
@@ -17,7 +17,12 @@ module Mongoid
|
|
|
17
17
|
#
|
|
18
18
|
# @return [ true | false ] True if matches, false if not.
|
|
19
19
|
def _matches?(selector)
|
|
20
|
-
|
|
20
|
+
# Opens a regexp budget for this document only. Callers that match many
|
|
21
|
+
# documents against one selector open a budget of their own first, and
|
|
22
|
+
# this one joins it rather than giving every document a fresh limit.
|
|
23
|
+
Matcher::RegexpBudget.open(selector) do
|
|
24
|
+
Matcher::Expression.matches?(self, selector)
|
|
25
|
+
end
|
|
21
26
|
end
|
|
22
27
|
end
|
|
23
28
|
end
|
|
@@ -6,12 +6,10 @@ module Mongoid
|
|
|
6
6
|
#
|
|
7
7
|
# @api private
|
|
8
8
|
module EqImplWithRegexp
|
|
9
|
-
module_function def matches?(
|
|
9
|
+
module_function def matches?(_original_operator, value, condition)
|
|
10
10
|
case condition
|
|
11
|
-
when Regexp
|
|
12
|
-
value
|
|
13
|
-
when ::BSON::Regexp::Raw
|
|
14
|
-
value =~ condition.compile
|
|
11
|
+
when Regexp, ::BSON::Regexp::Raw
|
|
12
|
+
value.respond_to?(:=~) && RegexpBudget.match?(value, condition)
|
|
15
13
|
else
|
|
16
14
|
if Mongoid.compare_time_by_ms &&
|
|
17
15
|
value.kind_of?(Time) && condition.kind_of?(Time)
|
|
@@ -3,25 +3,24 @@ module Mongoid
|
|
|
3
3
|
|
|
4
4
|
# @api private
|
|
5
5
|
module Regex
|
|
6
|
-
module_function def matches?(
|
|
7
|
-
condition
|
|
8
|
-
when Regexp
|
|
9
|
-
condition
|
|
10
|
-
when BSON::Regexp::Raw
|
|
11
|
-
condition.compile
|
|
12
|
-
else
|
|
6
|
+
module_function def matches?(_exists, value, condition)
|
|
7
|
+
unless condition.is_a?(Regexp) || condition.is_a?(BSON::Regexp::Raw)
|
|
13
8
|
# Note that strings must have been converted to a regular expression
|
|
14
9
|
# instance already (with $options taken into account, if provided).
|
|
15
10
|
raise Errors::InvalidQuery, "$regex requires a regular expression argument: #{Errors::InvalidQuery.truncate_expr(condition)}"
|
|
16
11
|
end
|
|
17
12
|
|
|
13
|
+
# The condition is compiled by RegexpBudget rather than here, so that
|
|
14
|
+
# the budget's timeout can be baked into the pattern.
|
|
18
15
|
case value
|
|
19
16
|
when Array
|
|
17
|
+
# Object#=~ is gone as of Ruby 3.2, so an element that cannot be
|
|
18
|
+
# matched against has to be rejected rather than passed to =~.
|
|
20
19
|
value.any? do |v|
|
|
21
|
-
v
|
|
20
|
+
v.respond_to?(:=~) && RegexpBudget.match?(v, condition)
|
|
22
21
|
end
|
|
23
22
|
when String
|
|
24
|
-
value
|
|
23
|
+
RegexpBudget.match?(value, condition)
|
|
25
24
|
else
|
|
26
25
|
false
|
|
27
26
|
end
|