mongoid 9.1.0 → 9.1.2
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 +69 -0
- data/lib/mongoid/association/accessors.rb +27 -5
- 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 +65 -101
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- 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/buildable.rb +6 -0
- 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 +58 -0
- data/lib/mongoid/association.rb +15 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +9 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +170 -22
- 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/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +73 -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/invalid_encryption_schema.rb +27 -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 +30 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +5 -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/fields.rb +7 -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 +120 -14
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +77 -0
- data/lib/mongoid/search_indexable.rb +69 -24
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +33 -16
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +300 -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/accessors_spec.rb +35 -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/buildable_spec.rb +11 -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/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +278 -8
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/defaults_spec.rb +33 -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/includable_spec.rb +1221 -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 +115 -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/fields_spec.rb +8 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +321 -9
- data/spec/mongoid/tasks/database_spec.rb +31 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +242 -0
- data/spec/mongoid/touchable_spec_models.rb +47 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +253 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +55 -2
|
@@ -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/document.rb
CHANGED
|
@@ -410,6 +410,8 @@ module Mongoid
|
|
|
410
410
|
def instantiate_document(attrs = nil, selected_fields = nil, options = {}, &block)
|
|
411
411
|
execute_callbacks = options.fetch(:execute_callbacks, Threaded.execute_callbacks?)
|
|
412
412
|
attributes = attrs.to_h
|
|
413
|
+
preloaded_fields = attributes.keys.select { |key| key.start_with?(Association::PRELOADED_PREFIX) }
|
|
414
|
+
preloaded = attributes.extract!(*preloaded_fields)
|
|
413
415
|
|
|
414
416
|
doc = allocate
|
|
415
417
|
doc.__selected_fields = selected_fields
|
|
@@ -417,6 +419,7 @@ module Mongoid
|
|
|
417
419
|
doc.instance_variable_set(:@attributes_before_type_cast, attributes.dup)
|
|
418
420
|
|
|
419
421
|
doc._handle_callbacks_after_instantiation(execute_callbacks, &block)
|
|
422
|
+
doc.load_preloaded_associations(preloaded)
|
|
420
423
|
|
|
421
424
|
doc.remember_storage_options!
|
|
422
425
|
doc
|
data/lib/mongoid/encryptable.rb
CHANGED
|
@@ -36,6 +36,79 @@ 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
|
+
# This runs on the persistence path, so the answer is memoized. A true
|
|
47
|
+
# answer is always memoized. A false answer is memoized only when every
|
|
48
|
+
# embeds_one target in the walk resolved to a class; if one could not be
|
|
49
|
+
# resolved yet, the answer is recomputed next time, so a class loaded
|
|
50
|
+
# later is still detected. Declaring encryption on a model after it has
|
|
51
|
+
# already been persisted is not supported.
|
|
52
|
+
#
|
|
53
|
+
# @return [ true | false ] Whether the model needs an encryption schema.
|
|
54
|
+
#
|
|
55
|
+
# @api private
|
|
56
|
+
def requires_encryption_schema?
|
|
57
|
+
return @requires_encryption_schema if defined?(@requires_encryption_schema)
|
|
58
|
+
return @requires_encryption_schema = true if encrypted?
|
|
59
|
+
|
|
60
|
+
found, complete = embedded_encryption_scan([ self ])
|
|
61
|
+
@requires_encryption_schema = found if found || complete
|
|
62
|
+
found
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Walks the models reachable through this model's embeds_one relations,
|
|
66
|
+
# looking for one that declares encryption. Subclasses of an
|
|
67
|
+
# association target are included: any of them can be assigned to the
|
|
68
|
+
# association.
|
|
69
|
+
#
|
|
70
|
+
# embeds_many is not considered: libmongocrypt cannot express per-field
|
|
71
|
+
# encryption under array items, so those fields are never mapped.
|
|
72
|
+
#
|
|
73
|
+
# @param [ Array<Class> ] path The models the walk is already inside of.
|
|
74
|
+
# A model embedding itself terminates here.
|
|
75
|
+
#
|
|
76
|
+
# @return [ Array<true | false> ] A pair: whether an embedded model is
|
|
77
|
+
# encrypted, and whether every embeds_one target the walk needed
|
|
78
|
+
# resolved to a class. The second value is only meaningful when the
|
|
79
|
+
# first is false.
|
|
80
|
+
#
|
|
81
|
+
# @api private
|
|
82
|
+
def embedded_encryption_scan(path)
|
|
83
|
+
complete = true
|
|
84
|
+
|
|
85
|
+
relations.each_value do |relation|
|
|
86
|
+
next unless relation.is_a?(Association::Embedded::EmbedsOne)
|
|
87
|
+
|
|
88
|
+
klass = relation.try_relation_class
|
|
89
|
+
# The class an association names does not have to exist yet.
|
|
90
|
+
if klass.nil?
|
|
91
|
+
complete = false
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# An association target does not have to be a Mongoid document.
|
|
96
|
+
next unless klass.respond_to?(:encrypted?)
|
|
97
|
+
|
|
98
|
+
([ klass ] + klass.descendants).uniq.each do |candidate|
|
|
99
|
+
next if path.include?(candidate)
|
|
100
|
+
return [ true, true ] if candidate.encrypted?
|
|
101
|
+
|
|
102
|
+
found, nested_complete = candidate.embedded_encryption_scan(path + [ candidate ])
|
|
103
|
+
return [ true, true ] if found
|
|
104
|
+
|
|
105
|
+
complete &&= nested_complete
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
[ false, complete ]
|
|
110
|
+
end
|
|
111
|
+
|
|
39
112
|
# Override the key_id for the model.
|
|
40
113
|
#
|
|
41
114
|
# 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
|
|
@@ -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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Errors
|
|
5
|
+
# This error is raised when two models writing to the same namespace
|
|
6
|
+
# describe a field, or the collection metadata, differently. The schema map
|
|
7
|
+
# has one entry per namespace, so the two descriptions cannot both be
|
|
8
|
+
# honored.
|
|
9
|
+
class InvalidEncryptionSchema < MongoidError
|
|
10
|
+
# Create the new error.
|
|
11
|
+
#
|
|
12
|
+
# @example Create the error.
|
|
13
|
+
# InvalidEncryptionSchema.new('music.bands', 'properties.code')
|
|
14
|
+
#
|
|
15
|
+
# @param [ String | nil ] namespace The namespace being described.
|
|
16
|
+
# @param [ String ] path The conflicting path in the schema.
|
|
17
|
+
def initialize(namespace, path)
|
|
18
|
+
super(
|
|
19
|
+
compose_message(
|
|
20
|
+
'invalid_encryption_schema',
|
|
21
|
+
{ namespace: namespace || 'the collection', path: path }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Errors
|
|
5
|
+
# This error is raised when a model declares encrypted fields, but the
|
|
6
|
+
# namespace it is about to be persisted to is not covered by the automatic
|
|
7
|
+
# encryption schema of the client in use. Without a schema the driver has
|
|
8
|
+
# nothing to encrypt with, and the fields would be stored in plaintext.
|
|
9
|
+
class NoEncryptionSchema < MongoidError
|
|
10
|
+
# Create the new error.
|
|
11
|
+
#
|
|
12
|
+
# @example Create the error.
|
|
13
|
+
# NoEncryptionSchema.new(Band, 'music.bands', :default)
|
|
14
|
+
#
|
|
15
|
+
# @param [ Class ] klass The model class.
|
|
16
|
+
# @param [ String ] namespace The namespace the model resolved to.
|
|
17
|
+
# @param [ String | Symbol ] client The name of the client in use.
|
|
18
|
+
# @param [ String | nil ] field The encrypted field missing from the
|
|
19
|
+
# schema entry, if the namespace itself is covered.
|
|
20
|
+
def initialize(klass, namespace, client, field = nil)
|
|
21
|
+
super(
|
|
22
|
+
compose_message(
|
|
23
|
+
field ? 'no_encryption_schema_field' : 'no_encryption_schema',
|
|
24
|
+
{ klass: klass, namespace: namespace, client: client, field: field }
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Errors
|
|
5
|
+
# Raised when attempting to write to a :through association, which is
|
|
6
|
+
# read-only.
|
|
7
|
+
class ReadonlyAssociation < MongoidError
|
|
8
|
+
# Instantiate the exception.
|
|
9
|
+
#
|
|
10
|
+
# @example Create the error.
|
|
11
|
+
# ReadonlyAssociation.new(Physician, association)
|
|
12
|
+
#
|
|
13
|
+
# @param [ Class ] klass The owner class.
|
|
14
|
+
# @param [ Mongoid::Association::Relatable ] association The through association.
|
|
15
|
+
def initialize(klass, association)
|
|
16
|
+
super(
|
|
17
|
+
compose_message(
|
|
18
|
+
'readonly_association',
|
|
19
|
+
{
|
|
20
|
+
klass: klass,
|
|
21
|
+
name: association.name,
|
|
22
|
+
through: association.options[:through]
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/mongoid/errors.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'mongoid/errors/mongoid_error'
|
|
4
|
+
require 'mongoid/errors/config_redactor'
|
|
4
5
|
require 'mongoid/errors/ambiguous_relationship'
|
|
5
6
|
require 'mongoid/errors/attribute_not_loaded'
|
|
6
7
|
require 'mongoid/errors/callback'
|
|
@@ -11,6 +12,7 @@ require 'mongoid/errors/document_not_found'
|
|
|
11
12
|
require 'mongoid/errors/empty_config_file'
|
|
12
13
|
require 'mongoid/errors/immutable_attribute'
|
|
13
14
|
require 'mongoid/errors/in_memory_collation_not_supported'
|
|
15
|
+
require 'mongoid/errors/in_memory_regexp_timeout'
|
|
14
16
|
require 'mongoid/errors/invalid_auto_encryption_configuration'
|
|
15
17
|
require 'mongoid/errors/invalid_around_callback'
|
|
16
18
|
require 'mongoid/errors/invalid_async_query_executor'
|
|
@@ -18,6 +20,7 @@ require 'mongoid/errors/invalid_collection'
|
|
|
18
20
|
require 'mongoid/errors/invalid_config_file'
|
|
19
21
|
require 'mongoid/errors/invalid_config_option'
|
|
20
22
|
require 'mongoid/errors/invalid_dependent_strategy'
|
|
23
|
+
require 'mongoid/errors/invalid_encryption_schema'
|
|
21
24
|
require 'mongoid/errors/invalid_field'
|
|
22
25
|
require 'mongoid/errors/invalid_field_option'
|
|
23
26
|
require 'mongoid/errors/invalid_field_type'
|
|
@@ -51,6 +54,7 @@ require 'mongoid/errors/mixed_client_configuration'
|
|
|
51
54
|
require 'mongoid/errors/nested_attributes_metadata_not_found'
|
|
52
55
|
require 'mongoid/errors/no_default_client'
|
|
53
56
|
require 'mongoid/errors/no_environment'
|
|
57
|
+
require 'mongoid/errors/no_encryption_schema'
|
|
54
58
|
require 'mongoid/errors/no_map_reduce_output'
|
|
55
59
|
require 'mongoid/errors/no_metadata'
|
|
56
60
|
require 'mongoid/errors/no_parent'
|
|
@@ -60,6 +64,7 @@ require 'mongoid/errors/no_client_database'
|
|
|
60
64
|
require 'mongoid/errors/no_client_hosts'
|
|
61
65
|
require 'mongoid/errors/readonly_attribute'
|
|
62
66
|
require 'mongoid/errors/readonly_document'
|
|
67
|
+
require 'mongoid/errors/readonly_association'
|
|
63
68
|
require 'mongoid/errors/rollback'
|
|
64
69
|
require 'mongoid/errors/sessions_not_supported'
|
|
65
70
|
require 'mongoid/errors/scope_overwrite'
|