mongoid 9.0.11 → 9.0.12

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +45 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/nested/many.rb +34 -5
  5. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  6. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  7. data/lib/mongoid/association/relatable.rb +17 -0
  8. data/lib/mongoid/collection_configurable.rb +8 -0
  9. data/lib/mongoid/config/encryption.rb +36 -18
  10. data/lib/mongoid/config.rb +56 -9
  11. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  12. data/lib/mongoid/contextual/memory.rb +18 -7
  13. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  14. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  15. data/lib/mongoid/encryptable.rb +46 -0
  16. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  17. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  18. data/lib/mongoid/errors.rb +2 -0
  19. data/lib/mongoid/field_readable.rb +70 -0
  20. data/lib/mongoid/indexable.rb +39 -27
  21. data/lib/mongoid/matchable.rb +6 -1
  22. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  23. data/lib/mongoid/matcher/regex.rb +9 -13
  24. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  25. data/lib/mongoid/matcher.rb +1 -0
  26. data/lib/mongoid/persistence_context.rb +35 -0
  27. data/lib/mongoid/search_indexable.rb +14 -7
  28. data/lib/mongoid/tasks/database.rb +23 -9
  29. data/lib/mongoid/threaded.rb +37 -0
  30. data/lib/mongoid/version.rb +1 -1
  31. data/spec/integration/app_spec.rb +7 -0
  32. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  33. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  34. data/spec/integration/encryption_spec.rb +149 -0
  35. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  36. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  37. data/spec/integration/query_operator_guard_spec.rb +89 -0
  38. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  39. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  40. data/spec/mongoid/attributes/nested_spec.rb +204 -10
  41. data/spec/mongoid/config/defaults_spec.rb +18 -0
  42. data/spec/mongoid/config/encryption_spec.rb +228 -0
  43. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  44. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  45. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  46. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  47. data/spec/mongoid/criteria_spec.rb +2 -0
  48. data/spec/mongoid/encryptable_spec.rb +61 -0
  49. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  50. data/spec/mongoid/tasks/database_spec.rb +18 -0
  51. data/spec/support/crypt/models.rb +157 -0
  52. metadata +14 -2
@@ -36,6 +36,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
36
36
  it_behaves_like 'requires a non-nil argument'
37
37
 
38
38
  context "when provided a string" do
39
+ # String criteria compile to $where, which requires the opt-in.
40
+ config_override :allow_unsafe_query_operators, true
39
41
 
40
42
  let(:selection) do
41
43
  query.where("this.value = 10")
@@ -587,4 +589,202 @@ describe Mongoid::Criteria::Queryable::Selectable do
587
589
  end
588
590
  end
589
591
  end
592
+
593
+ describe 'top-level operator injection guard' do
594
+ context 'when allow_unsafe_query_operators is true' do
595
+ config_override :allow_unsafe_query_operators, true
596
+
597
+ context 'when passing $where' do
598
+ it 'does not raise' do
599
+ expect do
600
+ query.where('$where' => 'this.name == "admin"')
601
+ end.not_to raise_error
602
+ end
603
+ end
604
+
605
+ context 'when passing a string criterion' do
606
+ it 'does not raise' do
607
+ expect do
608
+ query.where('this.name == "admin"')
609
+ end.not_to raise_error
610
+ end
611
+ end
612
+
613
+ context 'when passing a string criterion to a negated query' do
614
+ it 'does not raise' do
615
+ expect do
616
+ query.not.where('this.name == "admin"')
617
+ end.not_to raise_error
618
+ end
619
+ end
620
+
621
+ context 'when passing $function' do
622
+ it 'does not raise' do
623
+ expect do
624
+ query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
625
+ end.not_to raise_error
626
+ end
627
+ end
628
+ end
629
+
630
+ context 'when allow_unsafe_query_operators is false' do
631
+ config_override :allow_unsafe_query_operators, false
632
+
633
+ context 'when passing $where' do
634
+ it 'raises InvalidQuery' do
635
+ expect do
636
+ query.where('$where' => 'this.name == "admin"')
637
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
638
+ end
639
+ end
640
+
641
+ context 'when passing $function' do
642
+ it 'raises InvalidQuery' do
643
+ expect do
644
+ query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
645
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$function/)
646
+ end
647
+ end
648
+
649
+ context 'when the error mentions allow_unsafe_query_operators' do
650
+ it 'includes the config opt-in in the message' do
651
+ expect do
652
+ query.where('$where' => 'true')
653
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
654
+ end
655
+ end
656
+
657
+ context 'when passing a string criterion' do
658
+ it 'raises InvalidQuery' do
659
+ expect do
660
+ query.where('this.name == "admin"')
661
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
662
+ end
663
+
664
+ it 'includes the config opt-in in the message' do
665
+ expect do
666
+ query.where('this.name == "admin"')
667
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
668
+ end
669
+ end
670
+
671
+ context 'when passing a string criterion to a negated query' do
672
+ it 'raises InvalidQuery' do
673
+ expect do
674
+ query.not.where('this.name == "admin"')
675
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
676
+ end
677
+ end
678
+
679
+ %w[$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue].each do |op|
680
+ context "when passing #{op} (allowlisted)" do
681
+ it 'does not raise' do
682
+ value = case op
683
+ when '$and', '$or', '$nor', '$not' then [ { 'x' => 1 } ]
684
+ when '$text' then { '$search' => 'hi' }
685
+ when '$expr' then { '$gt' => [ '$a', 1 ] }
686
+ when '$jsonSchema' then { 'required' => [ 'x' ] }
687
+ else true
688
+ end
689
+ expect { query.where(op => value) }.not_to raise_error
690
+ end
691
+ end
692
+ end
693
+ end
694
+ end
695
+
696
+ describe 'nested operator injection guard' do
697
+ js = 'this.name == "admin"'
698
+
699
+ function_expr = {
700
+ '$function' => { 'body' => 'function() { return true; }', 'args' => [], 'lang' => 'js' }
701
+ }.freeze
702
+
703
+ accumulator_expr = {
704
+ '$accumulator' => {
705
+ 'init' => 'function() { return 0; }',
706
+ 'accumulate' => 'function() { return 0; }',
707
+ 'accumulateArgs' => [],
708
+ 'merge' => 'function() { return 0; }',
709
+ 'lang' => 'js'
710
+ }
711
+ }.freeze
712
+
713
+ # Every path into the selector that does not route through #expr_query,
714
+ # plus the nested forms that a top-level key check cannot see.
715
+ unsafe_queries = {
716
+ 'and with a $where key' => ->(query) { query.and('$where' => js) },
717
+ 'or with a $where key' => ->(query) { query.or('$where' => js) },
718
+ 'nor with a $where key' => ->(query) { query.nor('$where' => js) },
719
+ 'not with a $where key' => ->(query) { query.not('$where' => js) },
720
+ 'any_of with a $where key' => ->(query) { query.any_of('$where' => js) },
721
+ 'none_of with a $where key' => ->(query) { query.none_of('$where' => js) },
722
+ 'elem_match with a nested $where' => ->(query) { query.elem_match(a: { '$where' => js }) },
723
+ 'where with $where inside $or' => ->(query) { query.where('$or' => [ { '$where' => js } ]) },
724
+ 'where with $function inside $expr' => ->(query) { query.where('$expr' => function_expr) },
725
+ 'where with $accumulator inside $expr' => ->(query) { query.where('$expr' => accumulator_expr) },
726
+ 'where with $where three levels deep' => lambda { |query|
727
+ query.where('$and' => [ { '$or' => [ { '$where' => js } ] } ])
728
+ },
729
+ 'where with a symbol $where key inside $or' => ->(query) { query.where('$or' => [ { :$where => js } ]) }
730
+ }.freeze
731
+
732
+ context 'when allow_unsafe_query_operators is false' do
733
+ config_override :allow_unsafe_query_operators, false
734
+
735
+ unsafe_queries.each do |description, builder|
736
+ context "when querying with #{description}" do
737
+ it 'raises InvalidQuery' do
738
+ expect do
739
+ builder.call(query)
740
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where|\$function|\$accumulator/)
741
+ end
742
+ end
743
+ end
744
+
745
+ it 'includes the config opt-in in the message' do
746
+ expect do
747
+ query.or('$where' => js)
748
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
749
+ end
750
+
751
+ context 'when the nested expression is safe' do
752
+ it 'permits $or with field expressions' do
753
+ expect do
754
+ query.where('$or' => [ { 'a' => 1 }, { 'b' => { '$gt' => 2 } } ])
755
+ end.not_to raise_error
756
+ end
757
+
758
+ it 'permits $expr with aggregation operators' do
759
+ expect do
760
+ query.where('$expr' => { '$gt' => [ '$a', '$b' ] })
761
+ end.not_to raise_error
762
+ end
763
+
764
+ it 'permits a field whose name resembles a javascript operator' do
765
+ expect do
766
+ query.where('where' => js)
767
+ end.not_to raise_error
768
+ end
769
+
770
+ it 'permits a value that resembles a javascript operator' do
771
+ expect do
772
+ query.where('name' => '$where')
773
+ end.not_to raise_error
774
+ end
775
+ end
776
+ end
777
+
778
+ context 'when allow_unsafe_query_operators is true' do
779
+ config_override :allow_unsafe_query_operators, true
780
+
781
+ unsafe_queries.each do |description, builder|
782
+ context "when querying with #{description}" do
783
+ it 'does not raise' do
784
+ expect { builder.call(query) }.not_to raise_error
785
+ end
786
+ end
787
+ end
788
+ end
789
+ end
590
790
  end
@@ -2432,6 +2432,8 @@ describe Mongoid::Criteria do
2432
2432
  end
2433
2433
 
2434
2434
  context "when the criteria is not embedded" do
2435
+ # String criteria compile to $where, which requires the opt-in.
2436
+ config_override :allow_unsafe_query_operators, true
2435
2437
 
2436
2438
  let(:criteria) do
2437
2439
  Band.where("this.name == 'Depeche Mode'")
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'support/crypt/models'
5
+
6
+ describe Mongoid::Encryptable do
7
+ describe '.requires_encryption_schema?' do
8
+ context 'when the model declares encrypt_with' do
9
+ it 'returns true' do
10
+ expect(Crypt::Vault.requires_encryption_schema?).to be true
11
+ end
12
+ end
13
+
14
+ context 'when the model has an encrypted field' do
15
+ it 'returns true' do
16
+ expect(Crypt::User.requires_encryption_schema?).to be true
17
+ end
18
+ end
19
+
20
+ context 'when the model embeds an encrypted model' do
21
+ it 'returns true' do
22
+ expect(Crypt::Wallet.requires_encryption_schema?).to be true
23
+ end
24
+ end
25
+
26
+ context 'when the encrypted model is nested three levels deep' do
27
+ it 'returns true' do
28
+ expect(Crypt::Owner.requires_encryption_schema?).to be true
29
+ end
30
+ end
31
+
32
+ context 'when the model declares no encryption anywhere' do
33
+ it 'returns false' do
34
+ expect(Person.requires_encryption_schema?).to be false
35
+ end
36
+ end
37
+
38
+ # An association target does not have to be a Mongoid document. Truck
39
+ # embeds a plain Ruby class.
40
+ context 'when an embedded association target is not a document' do
41
+ it 'returns false' do
42
+ expect(Truck.requires_encryption_schema?).to be false
43
+ end
44
+ end
45
+
46
+ context 'when the model embeds itself' do
47
+ it 'terminates' do
48
+ expect(Crypt::Comment.requires_encryption_schema?).to be true
49
+ end
50
+ end
51
+
52
+ # Every model is asked this question, including ones naming an embedded
53
+ # class that is never defined. Such an association cannot be used, so
54
+ # nothing is embedded through it and nothing needs encrypting.
55
+ context 'when an embedded association names a class that is not defined' do
56
+ it 'returns false' do
57
+ expect(Crypt::Drawer.requires_encryption_schema?).to be false
58
+ end
59
+ end
60
+ end
61
+ end