mongoid 8.1.12 → 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/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/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/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/version.rb +1 -1
- data/lib/mongoid/warnings.rb +1 -0
- data/spec/integration/app_spec.rb +8 -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/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/matcher/regexp_budget_spec.rb +570 -0
- metadata +11 -2
|
@@ -35,6 +35,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
|
|
|
35
35
|
it_behaves_like 'requires a non-nil argument'
|
|
36
36
|
|
|
37
37
|
context "when provided a string" do
|
|
38
|
+
# String criteria compile to $where, which requires the opt-in.
|
|
39
|
+
config_override :allow_unsafe_query_operators, true
|
|
38
40
|
|
|
39
41
|
let(:selection) do
|
|
40
42
|
query.where("this.value = 10")
|
|
@@ -586,4 +588,237 @@ describe Mongoid::Criteria::Queryable::Selectable do
|
|
|
586
588
|
end
|
|
587
589
|
end
|
|
588
590
|
end
|
|
591
|
+
|
|
592
|
+
describe 'top-level operator injection guard' do
|
|
593
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
594
|
+
config_override :allow_unsafe_query_operators, true
|
|
595
|
+
|
|
596
|
+
context 'when passing $where' do
|
|
597
|
+
it 'does not raise' do
|
|
598
|
+
expect do
|
|
599
|
+
query.where('$where' => 'this.name == "admin"')
|
|
600
|
+
end.not_to raise_error
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
context 'when passing a string criterion' do
|
|
605
|
+
it 'does not raise' do
|
|
606
|
+
expect do
|
|
607
|
+
query.where('this.name == "admin"')
|
|
608
|
+
end.not_to raise_error
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
context 'when passing a string criterion to a negated query' do
|
|
613
|
+
it 'does not raise' do
|
|
614
|
+
expect do
|
|
615
|
+
query.not.where('this.name == "admin"')
|
|
616
|
+
end.not_to raise_error
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
context 'when passing $function' do
|
|
621
|
+
it 'does not raise' do
|
|
622
|
+
expect do
|
|
623
|
+
query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
|
|
624
|
+
end.not_to raise_error
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
context 'when allow_unsafe_query_operators is false' do
|
|
630
|
+
config_override :allow_unsafe_query_operators, false
|
|
631
|
+
|
|
632
|
+
context 'when passing $where' do
|
|
633
|
+
it 'raises InvalidQuery' do
|
|
634
|
+
expect do
|
|
635
|
+
query.where('$where' => 'this.name == "admin"')
|
|
636
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
context 'when passing $function' do
|
|
641
|
+
it 'raises InvalidQuery' do
|
|
642
|
+
expect do
|
|
643
|
+
query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
|
|
644
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$function/)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
context 'when the error mentions allow_unsafe_query_operators' do
|
|
649
|
+
it 'includes the config opt-in in the message' do
|
|
650
|
+
expect do
|
|
651
|
+
query.where('$where' => 'true')
|
|
652
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
context 'when passing a string criterion' do
|
|
657
|
+
it 'raises InvalidQuery' do
|
|
658
|
+
expect do
|
|
659
|
+
query.where('this.name == "admin"')
|
|
660
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it 'includes the config opt-in in the message' do
|
|
664
|
+
expect do
|
|
665
|
+
query.where('this.name == "admin"')
|
|
666
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
context 'when passing a string criterion to a negated query' do
|
|
671
|
+
it 'raises InvalidQuery' do
|
|
672
|
+
expect do
|
|
673
|
+
query.not.where('this.name == "admin"')
|
|
674
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
%w[$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue].each do |op|
|
|
679
|
+
context "when passing #{op} (allowlisted)" do
|
|
680
|
+
it 'does not raise' do
|
|
681
|
+
value = case op
|
|
682
|
+
when '$and', '$or', '$nor', '$not' then [ { 'x' => 1 } ]
|
|
683
|
+
when '$text' then { '$search' => 'hi' }
|
|
684
|
+
when '$expr' then { '$gt' => [ '$a', 1 ] }
|
|
685
|
+
when '$jsonSchema' then { 'required' => [ 'x' ] }
|
|
686
|
+
else true
|
|
687
|
+
end
|
|
688
|
+
expect { query.where(op => value) }.not_to raise_error
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
describe 'nested operator injection guard' do
|
|
696
|
+
js = 'this.name == "admin"'
|
|
697
|
+
|
|
698
|
+
function_expr = {
|
|
699
|
+
'$function' => { 'body' => 'function() { return true; }', 'args' => [], 'lang' => 'js' }
|
|
700
|
+
}.freeze
|
|
701
|
+
|
|
702
|
+
accumulator_expr = {
|
|
703
|
+
'$accumulator' => {
|
|
704
|
+
'init' => 'function() { return 0; }',
|
|
705
|
+
'accumulate' => 'function() { return 0; }',
|
|
706
|
+
'accumulateArgs' => [],
|
|
707
|
+
'merge' => 'function() { return 0; }',
|
|
708
|
+
'lang' => 'js'
|
|
709
|
+
}
|
|
710
|
+
}.freeze
|
|
711
|
+
|
|
712
|
+
# Every path into the selector that does not route through #expr_query,
|
|
713
|
+
# plus the nested forms that a top-level key check cannot see.
|
|
714
|
+
unsafe_queries = {
|
|
715
|
+
'and with a $where key' => ->(query) { query.and('$where' => js) },
|
|
716
|
+
'or with a $where key' => ->(query) { query.or('$where' => js) },
|
|
717
|
+
'nor with a $where key' => ->(query) { query.nor('$where' => js) },
|
|
718
|
+
'not with a $where key' => ->(query) { query.not('$where' => js) },
|
|
719
|
+
'any_of with a $where key' => ->(query) { query.any_of('$where' => js) },
|
|
720
|
+
'none_of with a $where key' => ->(query) { query.none_of('$where' => js) },
|
|
721
|
+
'elem_match with a nested $where' => ->(query) { query.elem_match(a: { '$where' => js }) },
|
|
722
|
+
'where with $where inside $or' => ->(query) { query.where('$or' => [ { '$where' => js } ]) },
|
|
723
|
+
'where with $function inside $expr' => ->(query) { query.where('$expr' => function_expr) },
|
|
724
|
+
'where with $accumulator inside $expr' => ->(query) { query.where('$expr' => accumulator_expr) },
|
|
725
|
+
'where with $where three levels deep' => lambda { |query|
|
|
726
|
+
query.where('$and' => [ { '$or' => [ { '$where' => js } ] } ])
|
|
727
|
+
},
|
|
728
|
+
'where with a symbol $where key inside $or' => ->(query) { query.where('$or' => [ { :$where => js } ]) }
|
|
729
|
+
}.freeze
|
|
730
|
+
|
|
731
|
+
context 'when allow_unsafe_query_operators is false' do
|
|
732
|
+
config_override :allow_unsafe_query_operators, false
|
|
733
|
+
|
|
734
|
+
unsafe_queries.each do |description, builder|
|
|
735
|
+
context "when querying with #{description}" do
|
|
736
|
+
it 'raises InvalidQuery' do
|
|
737
|
+
expect do
|
|
738
|
+
builder.call(query)
|
|
739
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where|\$function|\$accumulator/)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
it 'includes the config opt-in in the message' do
|
|
745
|
+
expect do
|
|
746
|
+
query.or('$where' => js)
|
|
747
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
context 'when the nested expression is safe' do
|
|
751
|
+
it 'permits $or with field expressions' do
|
|
752
|
+
expect do
|
|
753
|
+
query.where('$or' => [ { 'a' => 1 }, { 'b' => { '$gt' => 2 } } ])
|
|
754
|
+
end.not_to raise_error
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
it 'permits $expr with aggregation operators' do
|
|
758
|
+
expect do
|
|
759
|
+
query.where('$expr' => { '$gt' => [ '$a', '$b' ] })
|
|
760
|
+
end.not_to raise_error
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
it 'permits a field whose name resembles a javascript operator' do
|
|
764
|
+
expect do
|
|
765
|
+
query.where('where' => js)
|
|
766
|
+
end.not_to raise_error
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
it 'permits a value that resembles a javascript operator' do
|
|
770
|
+
expect do
|
|
771
|
+
query.where('name' => '$where')
|
|
772
|
+
end.not_to raise_error
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
778
|
+
config_override :allow_unsafe_query_operators, true
|
|
779
|
+
|
|
780
|
+
unsafe_queries.each do |description, builder|
|
|
781
|
+
context "when querying with #{description}" do
|
|
782
|
+
it 'does not raise' do
|
|
783
|
+
expect { builder.call(query) }.not_to raise_error
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
describe 'operator guard on chained override methods' do
|
|
791
|
+
# On this branch overwrite_chained_operators defaults to false, which
|
|
792
|
+
# routes elem_match through the guarded and_with_operator funnel. Force
|
|
793
|
+
# the override path so the guard in Mergeable#__override__ is exercised.
|
|
794
|
+
config_override :overwrite_chained_operators, true
|
|
795
|
+
|
|
796
|
+
let(:js) { 'this.name == "admin"' }
|
|
797
|
+
|
|
798
|
+
context 'when allow_unsafe_query_operators is false' do
|
|
799
|
+
config_override :allow_unsafe_query_operators, false
|
|
800
|
+
|
|
801
|
+
it 'rejects a $where nested in elem_match' do
|
|
802
|
+
expect do
|
|
803
|
+
query.elem_match(a: { '$where' => js })
|
|
804
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /executes server-side JavaScript/)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
it 'permits an ordinary elem_match value' do
|
|
808
|
+
expect do
|
|
809
|
+
query.elem_match(a: { name: 'x' })
|
|
810
|
+
end.not_to raise_error
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
815
|
+
config_override :allow_unsafe_query_operators, true
|
|
816
|
+
|
|
817
|
+
it 'permits a $where nested in elem_match' do
|
|
818
|
+
expect do
|
|
819
|
+
query.elem_match(a: { '$where' => js })
|
|
820
|
+
end.not_to raise_error
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|
|
589
824
|
end
|
|
@@ -2659,6 +2659,8 @@ describe Mongoid::Criteria do
|
|
|
2659
2659
|
end
|
|
2660
2660
|
|
|
2661
2661
|
context "when the criteria is not embedded" do
|
|
2662
|
+
# String criteria compile to $where, which requires the opt-in.
|
|
2663
|
+
config_override :allow_unsafe_query_operators, true
|
|
2662
2664
|
|
|
2663
2665
|
let(:criteria) do
|
|
2664
2666
|
Band.where("this.name == 'Depeche Mode'")
|