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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +45 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/relatable.rb +17 -0
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/encryption.rb +36 -18
- data/lib/mongoid/config.rb +56 -9
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/encryptable.rb +46 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
- data/lib/mongoid/errors.rb +2 -0
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/indexable.rb +39 -27
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
- data/lib/mongoid/matcher/regex.rb +9 -13
- data/lib/mongoid/matcher/regexp_budget.rb +383 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +14 -7
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +37 -0
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/app_spec.rb +7 -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 +149 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/attributes/nested_spec.rb +204 -10
- data/spec/mongoid/config/defaults_spec.rb +18 -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/queryable/selectable_logical_spec.rb +2 -0
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/encryptable_spec.rb +61 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/support/crypt/models.rb +157 -0
- 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
|