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
|
@@ -37,37 +37,34 @@ describe 'Mongoid application tests' do
|
|
|
37
37
|
FileUtils.mkdir_p(TMP_BASE)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
context '
|
|
40
|
+
context 'generated application' do
|
|
41
41
|
context 'sinatra' do
|
|
42
42
|
it 'runs' do
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
clone_application(
|
|
46
|
-
'https://github.com/mongoid/mongoid-demo',
|
|
47
|
-
subdir: 'sinatra-minimal',
|
|
48
|
-
) do
|
|
49
|
-
|
|
43
|
+
create_sinatra_app('mongoid-sinatra-test') do
|
|
50
44
|
# JRuby needs a long timeout
|
|
51
45
|
start_app(%w(bundle exec ruby app.rb), 4567, 40) do |port|
|
|
52
46
|
uri = URI.parse('http://localhost:4567/posts')
|
|
53
47
|
resp = JSON.parse(uri.open.read)
|
|
54
48
|
|
|
55
49
|
resp.should == []
|
|
56
|
-
|
|
57
50
|
end
|
|
58
51
|
end
|
|
59
52
|
end
|
|
60
53
|
end
|
|
61
54
|
|
|
62
55
|
context 'rails-api' do
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
before(:all) do
|
|
57
|
+
# Rails 6.0/6.1 have Logger issues on Ruby 3.1+
|
|
58
|
+
# Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
|
|
59
|
+
if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
|
|
60
|
+
skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
|
|
61
|
+
elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
|
|
62
|
+
skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
|
|
63
|
+
end
|
|
64
|
+
end
|
|
70
65
|
|
|
66
|
+
it 'runs' do
|
|
67
|
+
create_rails_api_app('mongoid-rails-api-test') do
|
|
71
68
|
# JRuby needs a long timeout
|
|
72
69
|
start_app(%w(bundle exec rails s), 3000, 50) do |port|
|
|
73
70
|
uri = URI.parse('http://localhost:3000/posts')
|
|
@@ -114,7 +111,7 @@ describe 'Mongoid application tests' do
|
|
|
114
111
|
|
|
115
112
|
Dir.chdir(TMP_BASE) do
|
|
116
113
|
FileUtils.rm_rf(name)
|
|
117
|
-
check_call(insert_rails_gem_version(%W(rails new #{name} --skip-spring --skip-active-record)), env:
|
|
114
|
+
check_call(insert_rails_gem_version(%W(rails new #{name} --skip-spring --skip-active-record)), env: rails_env)
|
|
118
115
|
|
|
119
116
|
Dir.chdir(name) do
|
|
120
117
|
adjust_rails_defaults
|
|
@@ -126,10 +123,149 @@ describe 'Mongoid application tests' do
|
|
|
126
123
|
end
|
|
127
124
|
end
|
|
128
125
|
|
|
126
|
+
def create_sinatra_app(name)
|
|
127
|
+
Dir.chdir(TMP_BASE) do
|
|
128
|
+
FileUtils.rm_rf(name)
|
|
129
|
+
FileUtils.mkdir_p(name)
|
|
130
|
+
|
|
131
|
+
Dir.chdir(name) do
|
|
132
|
+
# Create minimal Sinatra app with Post model
|
|
133
|
+
File.open('app.rb', 'w') do |f|
|
|
134
|
+
f.write(<<~RUBY)
|
|
135
|
+
require 'sinatra'
|
|
136
|
+
require 'mongoid'
|
|
137
|
+
require 'json'
|
|
138
|
+
|
|
139
|
+
Mongoid.load!('config/mongoid.yml')
|
|
140
|
+
|
|
141
|
+
class Post
|
|
142
|
+
include Mongoid::Document
|
|
143
|
+
field :title, type: String
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
get '/posts' do
|
|
147
|
+
content_type :json
|
|
148
|
+
Post.all.to_json
|
|
149
|
+
end
|
|
150
|
+
RUBY
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Create Gemfile
|
|
154
|
+
File.open('Gemfile', 'w') do |f|
|
|
155
|
+
f.write(<<~RUBY)
|
|
156
|
+
source 'https://rubygems.org'
|
|
157
|
+
gem 'sinatra'
|
|
158
|
+
gem 'rackup'
|
|
159
|
+
gem 'mongoid', path: '#{File.expand_path(BASE)}'
|
|
160
|
+
gem 'puma'
|
|
161
|
+
RUBY
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
FileUtils.mkdir_p('config')
|
|
165
|
+
write_mongoid_yml
|
|
166
|
+
check_call(%w(bundle install), env: clean_env)
|
|
167
|
+
|
|
168
|
+
yield
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def create_rails_api_app(name)
|
|
174
|
+
install_rails
|
|
175
|
+
|
|
176
|
+
Dir.chdir(TMP_BASE) do
|
|
177
|
+
FileUtils.rm_rf(name)
|
|
178
|
+
check_call(insert_rails_gem_version(%W(rails new #{name} --api --skip-spring --skip-active-record)), env: rails_env)
|
|
179
|
+
|
|
180
|
+
Dir.chdir(name) do
|
|
181
|
+
adjust_rails_defaults
|
|
182
|
+
adjust_app_gemfile
|
|
183
|
+
|
|
184
|
+
# Create Post model
|
|
185
|
+
File.open('app/models/post.rb', 'w') do |f|
|
|
186
|
+
f.write(<<~RUBY)
|
|
187
|
+
class Post
|
|
188
|
+
include Mongoid::Document
|
|
189
|
+
field :title, type: String
|
|
190
|
+
end
|
|
191
|
+
RUBY
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Create PostsController
|
|
195
|
+
File.open('app/controllers/posts_controller.rb', 'w') do |f|
|
|
196
|
+
f.write(<<~RUBY)
|
|
197
|
+
class PostsController < ApplicationController
|
|
198
|
+
def index
|
|
199
|
+
render json: Post.all
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
RUBY
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Add route
|
|
206
|
+
routes_content = File.read('config/routes.rb')
|
|
207
|
+
routes_content.sub!(/Rails\.application\.routes\.draw do\n/,
|
|
208
|
+
"Rails.application.routes.draw do\n resources :posts, only: [:index]\n")
|
|
209
|
+
File.open('config/routes.rb', 'w') { |f| f.write(routes_content) }
|
|
210
|
+
|
|
211
|
+
write_mongoid_yml
|
|
212
|
+
check_call(%w(bundle install), env: clean_env)
|
|
213
|
+
|
|
214
|
+
yield
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def create_rails_rake_test_app(name)
|
|
220
|
+
install_rails
|
|
221
|
+
|
|
222
|
+
Dir.chdir(TMP_BASE) do
|
|
223
|
+
FileUtils.rm_rf(name)
|
|
224
|
+
check_call(insert_rails_gem_version(%W(rails new #{name} --api --skip-spring --skip-active-record)), env: rails_env)
|
|
225
|
+
|
|
226
|
+
Dir.chdir(name) do
|
|
227
|
+
adjust_rails_defaults
|
|
228
|
+
adjust_app_gemfile
|
|
229
|
+
|
|
230
|
+
# Create Post model with index
|
|
231
|
+
File.open('app/models/post.rb', 'w') do |f|
|
|
232
|
+
f.write(<<~RUBY)
|
|
233
|
+
class Post
|
|
234
|
+
include Mongoid::Document
|
|
235
|
+
include Mongoid::Timestamps
|
|
236
|
+
field :subject, type: String
|
|
237
|
+
field :message, type: String
|
|
238
|
+
|
|
239
|
+
index subject: 1
|
|
240
|
+
end
|
|
241
|
+
RUBY
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Create Comment model
|
|
245
|
+
File.open('app/models/comment.rb', 'w') do |f|
|
|
246
|
+
f.write(<<~RUBY)
|
|
247
|
+
class Comment
|
|
248
|
+
include Mongoid::Document
|
|
249
|
+
include Mongoid::Timestamps
|
|
250
|
+
belongs_to :post
|
|
251
|
+
end
|
|
252
|
+
RUBY
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
write_mongoid_yml
|
|
256
|
+
check_call(%w(bundle install), env: clean_env)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
129
261
|
context 'new application - rails' do
|
|
130
262
|
before(:all) do
|
|
131
|
-
|
|
132
|
-
|
|
263
|
+
# Rails 6.0/6.1 have Logger issues on Ruby 3.1+
|
|
264
|
+
# Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
|
|
265
|
+
if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
|
|
266
|
+
skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
|
|
267
|
+
elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
|
|
268
|
+
skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
|
|
133
269
|
end
|
|
134
270
|
end
|
|
135
271
|
|
|
@@ -186,17 +322,31 @@ describe 'Mongoid application tests' do
|
|
|
186
322
|
if (rails_version = SpecConfig.instance.rails_version) == 'master'
|
|
187
323
|
else
|
|
188
324
|
check_call(%w(gem list))
|
|
325
|
+
|
|
326
|
+
# Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
|
|
327
|
+
if rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
|
|
328
|
+
check_call(%w(gem install logger --no-document))
|
|
329
|
+
end
|
|
330
|
+
|
|
189
331
|
check_call(%w(gem install rails --no-document --force -v) + ["~> #{rails_version}.0"])
|
|
190
332
|
end
|
|
191
333
|
end
|
|
192
334
|
|
|
193
|
-
context '
|
|
335
|
+
context 'generated test applications' do
|
|
336
|
+
before(:all) do
|
|
337
|
+
# Rails 6.0/6.1 have Logger issues on Ruby 3.1+
|
|
338
|
+
# Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
|
|
339
|
+
if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
|
|
340
|
+
skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
|
|
341
|
+
elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
|
|
342
|
+
skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
194
346
|
let(:client) { Mongoid.default_client }
|
|
195
347
|
|
|
196
348
|
describe 'create_indexes rake task' do
|
|
197
349
|
|
|
198
|
-
APP_PATH = File.join(File.dirname(__FILE__), '../../test-apps/rails-api')
|
|
199
|
-
|
|
200
350
|
%w(development production).each do |rails_env|
|
|
201
351
|
context "in #{rails_env}" do
|
|
202
352
|
|
|
@@ -207,20 +357,11 @@ describe 'Mongoid application tests' do
|
|
|
207
357
|
clean_env.merge(RAILS_ENV: rails_env, AUTOLOADER: autoloader)
|
|
208
358
|
end
|
|
209
359
|
|
|
360
|
+
let(:app_name) { "mongoid-rake-test-#{rails_env}-#{autoloader}" }
|
|
361
|
+
let(:app_path) { File.join(TMP_BASE, app_name) }
|
|
362
|
+
|
|
210
363
|
before do
|
|
211
|
-
|
|
212
|
-
remove_bundler_req
|
|
213
|
-
|
|
214
|
-
if BSON::Environment.jruby?
|
|
215
|
-
# Remove existing Gemfile.lock - see
|
|
216
|
-
# https://github.com/rubygems/rubygems/issues/3231
|
|
217
|
-
require 'fileutils'
|
|
218
|
-
FileUtils.rm_f('Gemfile.lock')
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
check_call(%w(bundle install), env: env)
|
|
222
|
-
write_mongoid_yml
|
|
223
|
-
end
|
|
364
|
+
create_rails_rake_test_app(app_name)
|
|
224
365
|
|
|
225
366
|
client['posts'].drop
|
|
226
367
|
client['posts'].create
|
|
@@ -233,7 +374,7 @@ describe 'Mongoid application tests' do
|
|
|
233
374
|
index.should be nil
|
|
234
375
|
|
|
235
376
|
check_call(%w(bundle exec rake db:mongoid:create_indexes -t),
|
|
236
|
-
cwd:
|
|
377
|
+
cwd: app_path, env: env)
|
|
237
378
|
|
|
238
379
|
index = client['posts'].indexes.detect do |index|
|
|
239
380
|
index['key'] == {'subject' => 1}
|
|
@@ -317,6 +458,19 @@ describe 'Mongoid application tests' do
|
|
|
317
458
|
line =~ /mongoid/
|
|
318
459
|
end
|
|
319
460
|
gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n"
|
|
461
|
+
|
|
462
|
+
# json 3.0 removed the quirks_mode and max_nesting keywords that
|
|
463
|
+
# ActiveSupport::JSON (every 7.x) still passes to JSON.generate and
|
|
464
|
+
# JSON.parse, so a freshly resolved bundle picks a 3.x json and the app
|
|
465
|
+
# fails to encode any response with ArgumentError: unknown keyword:
|
|
466
|
+
# quirks_mode. Cap json at 2.x, which still accepts those keywords.
|
|
467
|
+
gemfile_lines << "gem 'json', '< 3'\n"
|
|
468
|
+
|
|
469
|
+
# Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
|
|
470
|
+
if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
|
|
471
|
+
gemfile_lines << "gem 'logger'\n"
|
|
472
|
+
end
|
|
473
|
+
|
|
320
474
|
if rails_version
|
|
321
475
|
gemfile_lines.delete_if do |line|
|
|
322
476
|
line =~ /gem ['"]rails['"]/
|
|
@@ -376,6 +530,17 @@ describe 'Mongoid application tests' do
|
|
|
376
530
|
@clean_env ||= Hash[ENV.keys.grep(/BUNDLE|RUBYOPT/).map { |k| [k, nil ] }]
|
|
377
531
|
end
|
|
378
532
|
|
|
533
|
+
def rails_env
|
|
534
|
+
# For Rails 6.0/6.1 on Ruby 2.7+, we need to require logger
|
|
535
|
+
# to fix "uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger"
|
|
536
|
+
env = clean_env.dup
|
|
537
|
+
rails_version = SpecConfig.instance.rails_version
|
|
538
|
+
if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
|
|
539
|
+
env['RUBYOPT'] = '-rlogger'
|
|
540
|
+
end
|
|
541
|
+
env
|
|
542
|
+
end
|
|
543
|
+
|
|
379
544
|
def wait_for_port(port, timeout, process)
|
|
380
545
|
deadline = Mongoid::Utils.monotonic_time + timeout
|
|
381
546
|
loop do
|
|
@@ -87,8 +87,23 @@ describe 'has_and_belongs_to_many associations' do
|
|
|
87
87
|
})
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
# The nested attributes are processed before the attachment_ids
|
|
91
|
+
# assignment is applied, so at that point the id is not yet in the
|
|
92
|
+
# association and resolving it requires a collection-wide lookup.
|
|
93
|
+
context 'when allow_reparenting_via_nested_attributes is false' do
|
|
94
|
+
config_override :allow_reparenting_via_nested_attributes, false
|
|
95
|
+
|
|
96
|
+
it 'raises a document not found error' do
|
|
97
|
+
expect { image_block.save! }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
102
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
103
|
+
|
|
104
|
+
it 'does not raise on save' do
|
|
105
|
+
expect { image_block.save! }.not_to raise_error
|
|
106
|
+
end
|
|
92
107
|
end
|
|
93
108
|
end
|
|
94
109
|
|
|
@@ -268,10 +268,20 @@ describe "Dots and Dollars" do
|
|
|
268
268
|
DADMUser.where("$_amount": 0).first
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
-
it
|
|
271
|
+
it 'raises an error' do
|
|
272
272
|
expect do
|
|
273
273
|
queried
|
|
274
|
-
end.to raise_error(
|
|
274
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
278
|
+
config_override :allow_unsafe_query_operators, true
|
|
279
|
+
|
|
280
|
+
it 'raises an error from the server' do
|
|
281
|
+
expect do
|
|
282
|
+
queried
|
|
283
|
+
end.to raise_error(Mongo::Error::OperationFailure)
|
|
284
|
+
end
|
|
275
285
|
end
|
|
276
286
|
end
|
|
277
287
|
end
|
|
@@ -34,19 +34,24 @@ describe 'Encryption' do
|
|
|
34
34
|
around do |example|
|
|
35
35
|
Mongoid.default_client[Crypt::Patient.collection_name].drop
|
|
36
36
|
Mongoid.default_client[Crypt::Car.collection_name].drop
|
|
37
|
+
Mongoid.default_client[Crypt::Folder.collection_name].drop
|
|
37
38
|
existing_key_id = Crypt::Patient.encrypt_metadata[:key_id]
|
|
38
39
|
Crypt::Patient.set_key_id(data_key_id)
|
|
39
40
|
Crypt::Car.set_key_id(data_key_id)
|
|
41
|
+
Crypt::Note.set_key_id(data_key_id)
|
|
40
42
|
Mongoid::Config.send(:clients=, config)
|
|
41
43
|
Mongoid::Clients.with_name(:key_vault)[key_vault_collection].drop
|
|
42
44
|
Crypt::Patient.store_in(client: :encrypted)
|
|
43
45
|
Crypt::Car.store_in(client: :encrypted, database: Crypt::Car.storage_options[:database])
|
|
46
|
+
Crypt::Folder.store_in(client: :encrypted)
|
|
44
47
|
|
|
45
48
|
example.run
|
|
46
49
|
|
|
47
50
|
Crypt::Patient.reset_storage_options!
|
|
51
|
+
Crypt::Folder.reset_storage_options!
|
|
48
52
|
Crypt::Patient.set_key_id(existing_key_id)
|
|
49
53
|
Crypt::Car.set_key_id(existing_key_id)
|
|
54
|
+
Crypt::Note.set_key_id(existing_key_id)
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
it 'encrypts and decrypts fields' do
|
|
@@ -86,6 +91,17 @@ describe 'Encryption' do
|
|
|
86
91
|
end
|
|
87
92
|
end
|
|
88
93
|
|
|
94
|
+
# Nothing about the parent document says it needs a schema: it has no
|
|
95
|
+
# encrypted field of its own and no encrypt_with. Its collection still needs
|
|
96
|
+
# one, or the embedded field goes out in the clear.
|
|
97
|
+
it 'stores an embedded field encrypted when the parent has no encrypted field' do
|
|
98
|
+
folder = Crypt::Folder.create!(note: Crypt::Note.new(text: 'SECRET'))
|
|
99
|
+
unencrypted_client[Crypt::Folder.collection.name].find(_id: folder.id).first.tap do |doc|
|
|
100
|
+
expect(doc['note']['text']).to be_a(BSON::Binary)
|
|
101
|
+
expect(doc['note']['text'].type).to eq(:ciphertext)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
89
105
|
it 'stores data encrypted in the non-default database' do
|
|
90
106
|
car = Crypt::Car.create!(vin: 'VA1234')
|
|
91
107
|
unencrypted_client
|
|
@@ -96,5 +112,138 @@ describe 'Encryption' do
|
|
|
96
112
|
end
|
|
97
113
|
end
|
|
98
114
|
|
|
115
|
+
# The encryption schema map is keyed by namespace and built once, when the
|
|
116
|
+
# client is constructed. Whenever the namespace an encrypted model actually
|
|
117
|
+
# writes to is not in that map, the driver encrypts nothing and reports no
|
|
118
|
+
# error, so the field lands in the clear. Mongoid fails closed instead.
|
|
119
|
+
describe 'when the target namespace is not in the encryption schema map' do
|
|
120
|
+
let(:scratch_databases) do
|
|
121
|
+
%w[vehicles_dynamic vehicles_tenant_b vehicles_tenant_c tenant_a]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
around do |example|
|
|
125
|
+
original_options = {
|
|
126
|
+
Crypt::DynamicCar => Crypt::DynamicCar.storage_options,
|
|
127
|
+
Crypt::TenantCar => Crypt::TenantCar.storage_options
|
|
128
|
+
}
|
|
129
|
+
existing_key_ids = original_options.keys.to_h { |model| [ model, model.encrypt_metadata[:key_id] ] }
|
|
130
|
+
original_options.each_key do |model|
|
|
131
|
+
model.set_key_id(data_key_id)
|
|
132
|
+
model.store_in(client: :encrypted)
|
|
133
|
+
end
|
|
134
|
+
clean_scratch_data
|
|
135
|
+
# The schema map is generated when the client is built, so the client has
|
|
136
|
+
# to be built after the storage options above are in place.
|
|
137
|
+
Mongoid::Clients.clear
|
|
138
|
+
|
|
139
|
+
example.run
|
|
140
|
+
|
|
141
|
+
clean_scratch_data
|
|
142
|
+
original_options.each { |model, options| model.storage_options = options }
|
|
143
|
+
existing_key_ids.each { |model, key_id| model.set_key_id(key_id) }
|
|
144
|
+
Mongoid::Clients.clear
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def clean_scratch_data
|
|
148
|
+
scratch_databases.each { |database| unencrypted_client.use(database).database.drop }
|
|
149
|
+
# Crypt::Car writes into the shared 'vehicles' database, which other
|
|
150
|
+
# examples rely on, so remove only the documents these examples create.
|
|
151
|
+
unencrypted_client
|
|
152
|
+
.use(Crypt::Car.storage_options[:database])[Crypt::Car.collection_name.to_s]
|
|
153
|
+
.delete_many(vin: { '$in' => %w[CLIENT-1 CLIENT-2] })
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Reads with a client that has no automatic encryption, to see what really
|
|
157
|
+
# landed on disk.
|
|
158
|
+
def documents_with_plaintext_vin(database, collection, vin)
|
|
159
|
+
unencrypted_client.use(database)[collection].find(vin: vin).to_a
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
context 'when the database name is a callable' do
|
|
163
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
164
|
+
expect { Crypt::DynamicCar.create!(vin: 'DYNAMIC-1') }
|
|
165
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
166
|
+
|
|
167
|
+
expect(
|
|
168
|
+
documents_with_plaintext_vin('vehicles_dynamic', Crypt::DynamicCar.collection_name.to_s, 'DYNAMIC-1')
|
|
169
|
+
).to be_empty
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context 'when the database name is a callable resolved per tenant' do
|
|
174
|
+
around do |example|
|
|
175
|
+
Thread.current[:tenant_database] = 'tenant_a'
|
|
176
|
+
example.run
|
|
177
|
+
Thread.current[:tenant_database] = nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
181
|
+
expect { Crypt::TenantCar.create!(vin: 'TENANT-1') }
|
|
182
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
183
|
+
|
|
184
|
+
expect(
|
|
185
|
+
documents_with_plaintext_vin('tenant_a', Crypt::TenantCar.collection_name.to_s, 'TENANT-1')
|
|
186
|
+
).to be_empty
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
context 'when the database is overridden for the block' do
|
|
191
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
192
|
+
expect { Crypt::Car.with(database: 'vehicles_tenant_b') { |car| car.create!(vin: 'BLOCK-1') } }
|
|
193
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
194
|
+
|
|
195
|
+
expect(
|
|
196
|
+
documents_with_plaintext_vin('vehicles_tenant_b', Crypt::Car.collection_name.to_s, 'BLOCK-1')
|
|
197
|
+
).to be_empty
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
context 'when the database is overridden globally' do
|
|
202
|
+
persistence_context_override :database, 'vehicles_tenant_c'
|
|
99
203
|
|
|
204
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
205
|
+
expect { Crypt::Car.create!(vin: 'GLOBAL-1') }
|
|
206
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
207
|
+
|
|
208
|
+
expect(
|
|
209
|
+
documents_with_plaintext_vin('vehicles_tenant_c', Crypt::Car.collection_name.to_s, 'GLOBAL-1')
|
|
210
|
+
).to be_empty
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
context 'when the model is routed to a client without automatic encryption' do
|
|
215
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
216
|
+
expect { Crypt::Car.with(client: :default) { |car| car.create!(vin: 'CLIENT-1') } }
|
|
217
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
218
|
+
|
|
219
|
+
expect(
|
|
220
|
+
documents_with_plaintext_vin('vehicles', Crypt::Car.collection_name.to_s, 'CLIENT-1')
|
|
221
|
+
).to be_empty
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
context 'when the parent has no encrypted field of its own' do
|
|
226
|
+
it 'does not store the embedded field in plaintext', :aggregate_failures do
|
|
227
|
+
expect { Crypt::Folder.with(client: :default) { |folder| folder.create!(note: Crypt::Note.new(text: 'EMBEDDED-1')) } }
|
|
228
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
unencrypted_client[Crypt::Folder.collection_name.to_s].find('note.text' => 'EMBEDDED-1').to_a
|
|
232
|
+
).to be_empty
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
context 'when the client is overridden globally to one without automatic encryption' do
|
|
237
|
+
persistence_context_override :client, :default
|
|
238
|
+
|
|
239
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
240
|
+
expect { Crypt::Car.create!(vin: 'CLIENT-2') }
|
|
241
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
242
|
+
|
|
243
|
+
expect(
|
|
244
|
+
documents_with_plaintext_vin('vehicles', Crypt::Car.collection_name.to_s, 'CLIENT-2')
|
|
245
|
+
).to be_empty
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
100
249
|
end
|
|
@@ -135,6 +135,27 @@
|
|
|
135
135
|
$regex: bar
|
|
136
136
|
matches: false
|
|
137
137
|
|
|
138
|
+
# Object#=~ is gone as of Ruby 3.2, so an element that cannot be matched
|
|
139
|
+
# against has to be skipped rather than passed to =~.
|
|
140
|
+
- name: array field value - element that cannot be matched against
|
|
141
|
+
document:
|
|
142
|
+
title:
|
|
143
|
+
- 42
|
|
144
|
+
- foo
|
|
145
|
+
query:
|
|
146
|
+
title:
|
|
147
|
+
$regex: foo
|
|
148
|
+
matches: true
|
|
149
|
+
|
|
150
|
+
- name: array field value - no element can be matched against
|
|
151
|
+
document:
|
|
152
|
+
title:
|
|
153
|
+
- 42
|
|
154
|
+
query:
|
|
155
|
+
title:
|
|
156
|
+
$regex: foo
|
|
157
|
+
matches: false
|
|
158
|
+
|
|
138
159
|
- name: true field value
|
|
139
160
|
document:
|
|
140
161
|
title: true
|