mongoid 9.0.10 → 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/README.md +3 -3
- data/Rakefile +31 -0
- data/lib/config/locales/en.yml +45 -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/auto_save.rb +48 -6
- 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/defaults.rb +9 -5
- data/lib/mongoid/config/encryption.rb +36 -18
- data/lib/mongoid/config.rb +72 -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 +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/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +201 -36
- 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/auto_save_spec.rb +98 -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 +250 -30
- 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/errors/document_not_found_spec.rb +10 -3
- 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 +28 -2
|
@@ -582,4 +582,95 @@ describe Mongoid::Contextual::Aggregable::Memory do
|
|
|
582
582
|
end
|
|
583
583
|
end
|
|
584
584
|
end
|
|
585
|
+
|
|
586
|
+
context 'when the field name is a method name' do
|
|
587
|
+
let!(:depeche) do
|
|
588
|
+
Band.create!(name: 'Depeche Mode', likes: 1000)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
let(:criteria) do
|
|
592
|
+
Band.all.tap do |crit|
|
|
593
|
+
crit.documents = [ depeche ]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
it 'does not call the method for sum' do
|
|
598
|
+
expect(depeche).not_to receive(:delete)
|
|
599
|
+
expect(context.sum(:delete)).to eq(0)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
it 'does not call the method for min' do
|
|
603
|
+
expect(depeche).not_to receive(:delete)
|
|
604
|
+
expect(context.min(:delete)).to be_nil
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it 'does not call the method for max' do
|
|
608
|
+
expect(depeche).not_to receive(:delete)
|
|
609
|
+
expect(context.max(:delete)).to be_nil
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
it 'does not call the method for avg' do
|
|
613
|
+
expect(depeche).not_to receive(:delete)
|
|
614
|
+
expect(context.avg(:delete)).to be_nil
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
it 'does not call the method for aggregates' do
|
|
618
|
+
expect(depeche).not_to receive(:delete)
|
|
619
|
+
expect(context.aggregates(:delete))
|
|
620
|
+
.to eq('count' => 0, 'avg' => nil, 'max' => nil, 'min' => nil, 'sum' => 0)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
it 'does not call a private method for avg' do
|
|
624
|
+
expect(depeche).not_to receive(:exit)
|
|
625
|
+
expect(context.avg(:exit)).to be_nil
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
context 'when the field is not defined' do
|
|
630
|
+
let!(:depeche) do
|
|
631
|
+
Band.create!(name: 'Depeche Mode', likes: 1000)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
let(:criteria) do
|
|
635
|
+
Band.all.tap do |crit|
|
|
636
|
+
crit.documents = [ depeche ]
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
it 'returns zero for sum' do
|
|
641
|
+
expect(context.sum(:not_a_field)).to eq(0)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it 'returns nil for min' do
|
|
645
|
+
expect(context.min(:not_a_field)).to be_nil
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
it 'returns nil for max' do
|
|
649
|
+
expect(context.max(:not_a_field)).to be_nil
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
it 'returns nil for avg' do
|
|
653
|
+
expect(context.avg(:not_a_field)).to be_nil
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
context 'when the field is the foreign key of a many to many association' do
|
|
658
|
+
let!(:preference) do
|
|
659
|
+
Preference.create!(name: 'nature')
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
let!(:person) do
|
|
663
|
+
Person.new(preferences: [ preference ])
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
let(:criteria) do
|
|
667
|
+
Person.all.tap do |crit|
|
|
668
|
+
crit.documents = [ person ]
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
it 'aggregates the stored ids' do
|
|
673
|
+
expect(context.max(:preference_ids)).to eq([ preference.id ])
|
|
674
|
+
end
|
|
675
|
+
end
|
|
585
676
|
end
|
|
@@ -588,6 +588,27 @@ describe Mongoid::Contextual::Memory do
|
|
|
588
588
|
expect(context.distinct("label.sales")).to eq([ BigDecimal("1E2") ])
|
|
589
589
|
end
|
|
590
590
|
end
|
|
591
|
+
|
|
592
|
+
context 'when the field name is a method name' do
|
|
593
|
+
let!(:person) do
|
|
594
|
+
Person.create!(ssn: 'secret-ssn')
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
let!(:address) do
|
|
598
|
+
person.addresses.create!(street: 'hobrecht')
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
let(:criteria) do
|
|
602
|
+
Address.all.tap do |crit|
|
|
603
|
+
crit.documents = [ address ]
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it 'does not call the method' do
|
|
608
|
+
expect(address).not_to receive(:destroy)
|
|
609
|
+
expect(context.distinct(:destroy)).to eq([ nil ])
|
|
610
|
+
end
|
|
611
|
+
end
|
|
591
612
|
end
|
|
592
613
|
|
|
593
614
|
describe "#each" do
|
|
@@ -1826,6 +1847,116 @@ describe Mongoid::Contextual::Memory do
|
|
|
1826
1847
|
])
|
|
1827
1848
|
end
|
|
1828
1849
|
end
|
|
1850
|
+
|
|
1851
|
+
context 'when plucking a dynamic attribute' do
|
|
1852
|
+
let(:criteria) do
|
|
1853
|
+
Band.all.tap do |crit|
|
|
1854
|
+
crit.documents = [ Band.create!(name: 'Depeche Mode', mood: 'dark') ]
|
|
1855
|
+
end
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
it 'returns the value from the attributes' do
|
|
1859
|
+
expect(context.pluck(:mood)).to eq([ 'dark' ])
|
|
1860
|
+
end
|
|
1861
|
+
end
|
|
1862
|
+
|
|
1863
|
+
context 'when the field name is a method name' do
|
|
1864
|
+
let!(:person) do
|
|
1865
|
+
Person.create!(ssn: 'secret-ssn')
|
|
1866
|
+
end
|
|
1867
|
+
|
|
1868
|
+
let!(:address) do
|
|
1869
|
+
person.addresses.create!(street: 'hobrecht')
|
|
1870
|
+
end
|
|
1871
|
+
|
|
1872
|
+
let(:criteria) do
|
|
1873
|
+
Address.all.tap do |crit|
|
|
1874
|
+
crit.documents = [ address ]
|
|
1875
|
+
end
|
|
1876
|
+
end
|
|
1877
|
+
|
|
1878
|
+
it 'does not call the method' do
|
|
1879
|
+
expect(address).not_to receive(:destroy)
|
|
1880
|
+
expect(context.pluck(:destroy)).to eq([ nil ])
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
it 'does not return the attributes hash' do
|
|
1884
|
+
expect(context.pluck(:attributes)).to eq([ nil ])
|
|
1885
|
+
end
|
|
1886
|
+
|
|
1887
|
+
it 'does not traverse to the parent document' do
|
|
1888
|
+
expect(context.pluck('_root.ssn')).to eq([ nil ])
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
it 'does not destroy the parent document' do
|
|
1892
|
+
expect(context.pluck('_root.destroy')).to eq([ nil ])
|
|
1893
|
+
expect(Person.where(_id: person.id).count).to eq(1)
|
|
1894
|
+
end
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
context 'when plucking a belongs_to association' do
|
|
1898
|
+
let!(:band) do
|
|
1899
|
+
Band.create!(name: 'Depeche Mode')
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1902
|
+
let!(:artist) do
|
|
1903
|
+
Artist.create!(band: band)
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
let(:criteria) do
|
|
1907
|
+
Artist.all.tap do |crit|
|
|
1908
|
+
crit.documents = [ artist ]
|
|
1909
|
+
end
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1912
|
+
it 'traverses to the associated document' do
|
|
1913
|
+
expect(context.pluck('band.name')).to eq([ 'Depeche Mode' ])
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
it 'returns the document for the association name' do
|
|
1917
|
+
expect(context.pluck(:band)).to eq([ band ])
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
it 'returns the id for the foreign key' do
|
|
1921
|
+
expect(context.pluck(:band_id)).to eq([ band.id ])
|
|
1922
|
+
end
|
|
1923
|
+
end
|
|
1924
|
+
|
|
1925
|
+
context 'when plucking the foreign key of a many to many association' do
|
|
1926
|
+
let!(:preference) do
|
|
1927
|
+
Preference.create!(name: 'nature')
|
|
1928
|
+
end
|
|
1929
|
+
|
|
1930
|
+
let!(:person) do
|
|
1931
|
+
Person.create!(preferences: [ preference ])
|
|
1932
|
+
end
|
|
1933
|
+
|
|
1934
|
+
let(:criteria) do
|
|
1935
|
+
Person.all.tap do |crit|
|
|
1936
|
+
crit.documents = [ person ]
|
|
1937
|
+
end
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
it 'returns the stored ids' do
|
|
1941
|
+
expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
|
|
1942
|
+
end
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1945
|
+
context 'when the field path has an empty segment' do
|
|
1946
|
+
let!(:band) do
|
|
1947
|
+
Band.create!(name: 'Depeche Mode', label: Label.new(name: 'Mute'))
|
|
1948
|
+
end
|
|
1949
|
+
|
|
1950
|
+
let(:criteria) do
|
|
1951
|
+
Band.all.tap do |crit|
|
|
1952
|
+
crit.documents = [ band ]
|
|
1953
|
+
end
|
|
1954
|
+
end
|
|
1955
|
+
|
|
1956
|
+
it 'returns nil' do
|
|
1957
|
+
expect(context.pluck('label..name')).to eq([ nil ])
|
|
1958
|
+
end
|
|
1959
|
+
end
|
|
1829
1960
|
end
|
|
1830
1961
|
|
|
1831
1962
|
describe "#pick" do
|
|
@@ -1886,6 +2017,27 @@ describe Mongoid::Contextual::Memory do
|
|
|
1886
2017
|
expect(picked).to be_nil
|
|
1887
2018
|
end
|
|
1888
2019
|
end
|
|
2020
|
+
|
|
2021
|
+
context 'when the field name is a method name' do
|
|
2022
|
+
let!(:person) do
|
|
2023
|
+
Person.create!(ssn: 'secret-ssn')
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
let!(:address) do
|
|
2027
|
+
person.addresses.create!(street: 'hobrecht')
|
|
2028
|
+
end
|
|
2029
|
+
|
|
2030
|
+
let(:criteria) do
|
|
2031
|
+
Address.all.tap do |crit|
|
|
2032
|
+
crit.documents = [ address ]
|
|
2033
|
+
end
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
it 'does not call the method' do
|
|
2037
|
+
expect(address).not_to receive(:destroy)
|
|
2038
|
+
expect(context.pick(:destroy)).to be_nil
|
|
2039
|
+
end
|
|
2040
|
+
end
|
|
1889
2041
|
end
|
|
1890
2042
|
|
|
1891
2043
|
describe "#tally" do
|
|
@@ -2340,6 +2492,31 @@ describe Mongoid::Contextual::Memory do
|
|
|
2340
2492
|
)
|
|
2341
2493
|
end
|
|
2342
2494
|
end
|
|
2495
|
+
|
|
2496
|
+
context 'when the field name is a method name' do
|
|
2497
|
+
let!(:person) do
|
|
2498
|
+
Person.create!(ssn: 'secret-ssn')
|
|
2499
|
+
end
|
|
2500
|
+
|
|
2501
|
+
let!(:address) do
|
|
2502
|
+
person.addresses.create!(street: 'hobrecht')
|
|
2503
|
+
end
|
|
2504
|
+
|
|
2505
|
+
let(:criteria) do
|
|
2506
|
+
Address.all.tap do |crit|
|
|
2507
|
+
crit.documents = [ address ]
|
|
2508
|
+
end
|
|
2509
|
+
end
|
|
2510
|
+
|
|
2511
|
+
it 'does not call the method' do
|
|
2512
|
+
expect(address).not_to receive(:destroy)
|
|
2513
|
+
expect(context.tally(:destroy)).to eq(nil => 1)
|
|
2514
|
+
end
|
|
2515
|
+
|
|
2516
|
+
it 'does not disclose the parent document fields' do
|
|
2517
|
+
expect(context.tally('_root.ssn')).to eq(nil => 1)
|
|
2518
|
+
end
|
|
2519
|
+
end
|
|
2343
2520
|
end
|
|
2344
2521
|
|
|
2345
2522
|
describe '#inc' do
|
|
@@ -1830,6 +1830,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
|
|
|
1830
1830
|
end
|
|
1831
1831
|
|
|
1832
1832
|
context 'when the following criteria uses string were form' do
|
|
1833
|
+
# String criteria compile to $where, which requires the opt-in.
|
|
1834
|
+
config_override :allow_unsafe_query_operators, true
|
|
1833
1835
|
|
|
1834
1836
|
let(:selection) do
|
|
1835
1837
|
query.not.where('hello world')
|
|
@@ -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
|
|
@@ -64,9 +64,16 @@ describe Mongoid::Errors::DocumentNotFound do
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
it "contains the problem in the message" do
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
# Ruby 3.4+ changed Hash#inspect format from {:name=>"syd"} to {name: "syd"}
|
|
68
|
+
if RUBY_VERSION >= '3.4'
|
|
69
|
+
expect(error.message).to include(
|
|
70
|
+
"Document not found for class Person with attributes {name: \"syd\"}."
|
|
71
|
+
)
|
|
72
|
+
else
|
|
73
|
+
expect(error.message).to include(
|
|
74
|
+
"Document not found for class Person with attributes {:name=>\"syd\"}."
|
|
75
|
+
)
|
|
76
|
+
end
|
|
70
77
|
end
|
|
71
78
|
|
|
72
79
|
it "contains the summary in the message" do
|