railsmdb 1.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +65 -0
- data/lib/railsmdb/cli.rb +16 -0
- data/lib/railsmdb/commands/dbconsole/dbconsole_command.rb +65 -0
- data/lib/railsmdb/commands/setup/setup_command.rb +20 -0
- data/lib/railsmdb/commands.rb +5 -0
- data/lib/railsmdb/crypt_shared/catalog.rb +123 -0
- data/lib/railsmdb/crypt_shared/listing.rb +71 -0
- data/lib/railsmdb/downloader.rb +55 -0
- data/lib/railsmdb/ext/rails/command/behavior.rb +55 -0
- data/lib/railsmdb/ext/rails/command.rb +21 -0
- data/lib/railsmdb/ext/rails/generators/rails/app/app_generator.rb +48 -0
- data/lib/railsmdb/ext/rails/generators.rb +21 -0
- data/lib/railsmdb/extractor.rb +94 -0
- data/lib/railsmdb/generators/mongoid/model/model_generator.rb +41 -0
- data/lib/railsmdb/generators/mongoid/model/templates/model.rb.tt +19 -0
- data/lib/railsmdb/generators/setup/concerns/setuppable.rb +545 -0
- data/lib/railsmdb/generators/setup/setup_generator.rb +123 -0
- data/lib/railsmdb/generators/setup/templates/README.md.tt +3 -0
- data/lib/railsmdb/generators/setup/templates/_bin/railsmdb.tt +3 -0
- data/lib/railsmdb/generators/setup/templates/_config/initializers/mongoid.rb.tt +26 -0
- data/lib/railsmdb/prioritizable.rb +97 -0
- data/lib/railsmdb/version.rb +12 -0
- data.tar.gz.sig +4 -0
- metadata +182 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,545 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'railsmdb/prioritizable'
|
4
|
+
require 'active_support/encrypted_configuration'
|
5
|
+
require 'os'
|
6
|
+
require 'railsmdb/version'
|
7
|
+
require 'railsmdb/crypt_shared/catalog'
|
8
|
+
require 'railsmdb/downloader'
|
9
|
+
require 'railsmdb/extractor'
|
10
|
+
require 'rails/generators/rails/app/app_generator'
|
11
|
+
require 'digest'
|
12
|
+
require 'mongoid'
|
13
|
+
|
14
|
+
module Railsmdb
|
15
|
+
module Generators
|
16
|
+
module Setup
|
17
|
+
module Concerns
|
18
|
+
# Tasks used for configuring a Rails app to use Mongoid, including
|
19
|
+
# adding support for encryption. This concern is shared between
|
20
|
+
# the app generator (e.g. `railsmdb new`) and the setup generator
|
21
|
+
# (e.g. `railsmdb setup`).
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
module Setuppable
|
25
|
+
extend ActiveSupport::Concern
|
26
|
+
|
27
|
+
include Railsmdb::Prioritizable
|
28
|
+
|
29
|
+
GemfileEntry = Rails::Generators::AppBase::GemfileEntry
|
30
|
+
|
31
|
+
KEY_VAULT_CONFIG = <<~CONFIG
|
32
|
+
# This client is used to obtain the encryption keys from the key vault.
|
33
|
+
# For security reasons, this should be a different database instance than
|
34
|
+
# your primary application database.
|
35
|
+
key_vault:
|
36
|
+
uri: mongodb://localhost:27017
|
37
|
+
|
38
|
+
CONFIG
|
39
|
+
|
40
|
+
AUTO_ENCRYPTION_CONFIG = <<~CONFIG.freeze
|
41
|
+
# You can read about the auto encryption options here:
|
42
|
+
# https://www.mongodb.com/docs/ruby-driver/v#{Mongo::VERSION.split('.').first(2).join('.')}/reference/in-use-encryption/client-side-encryption/#auto-encryption-options
|
43
|
+
auto_encryption_options:
|
44
|
+
key_vault_client: 'key_vault'
|
45
|
+
key_vault_namespace: 'encryption.__keyVault'
|
46
|
+
kms_providers:
|
47
|
+
# Using a local master key is insecure and is not recommended if you plan
|
48
|
+
# to use client-side encryption in production.
|
49
|
+
#
|
50
|
+
# To learn how to set up a remote Key Management Service, see the tutorials
|
51
|
+
# at https://www.mongodb.com/docs/manual/core/csfle/tutorials/.
|
52
|
+
local:
|
53
|
+
key: '<%= Rails.application.credentials.mongodb_master_key %>'
|
54
|
+
extra_options:
|
55
|
+
crypt_shared_lib_path: %crypt-shared-path%
|
56
|
+
|
57
|
+
CONFIG
|
58
|
+
|
59
|
+
PRELOAD_MODELS_OPTION = <<~CONFIG
|
60
|
+
#
|
61
|
+
# Setting it to true is recommended for auto encryption to work
|
62
|
+
# properly in development.
|
63
|
+
preload_models: true
|
64
|
+
CONFIG
|
65
|
+
|
66
|
+
included do
|
67
|
+
# add the setup generator templates folder to the source path
|
68
|
+
source_paths.unshift File.join(__dir__, '..', 'templates')
|
69
|
+
|
70
|
+
# An option for enabling MongoDB encryption features in the new app.
|
71
|
+
class_option :encryption, type: :boolean,
|
72
|
+
aliases: '-E',
|
73
|
+
default: false,
|
74
|
+
desc: 'Add gems and configuration to enable MongoDB encryption features'
|
75
|
+
|
76
|
+
# Add an option for accepting the customer agreement related to
|
77
|
+
# MongoDB enterprise, allowing the acceptance prompt to be skipped.
|
78
|
+
class_option :accept_customer_agreement, type: :boolean,
|
79
|
+
default: false,
|
80
|
+
desc: 'Accept the MongoDB Customer Agreement'
|
81
|
+
end
|
82
|
+
|
83
|
+
# Save the current directory; this way, we can see
|
84
|
+
# if it is being run from the railsmdb project directory, in
|
85
|
+
# development, and set up the railsmdb gem dependency appropriately.
|
86
|
+
def save_initial_path
|
87
|
+
@initial_path = Dir.pwd
|
88
|
+
end
|
89
|
+
|
90
|
+
# Checks to see if the user agrees to the encryption terms and conditions
|
91
|
+
def confirm_legal_shenanigans
|
92
|
+
return unless options[:encryption]
|
93
|
+
|
94
|
+
@okay_to_support_encryption =
|
95
|
+
options[:accept_customer_agreement] ||
|
96
|
+
okay_with_legal_shenanigans?
|
97
|
+
end
|
98
|
+
|
99
|
+
# Fetches the MongoDB crypt_shared library and stores it in
|
100
|
+
# vendor/crypt_shared.
|
101
|
+
def fetch_crypt_shared
|
102
|
+
return unless @okay_to_support_encryption
|
103
|
+
|
104
|
+
log :fetch, 'current MongoDB catalog'
|
105
|
+
catalog = Railsmdb::CryptShared::Catalog.current
|
106
|
+
url, sha = catalog.optimal_download_url_for_this_host
|
107
|
+
|
108
|
+
if url
|
109
|
+
fetch_and_extract_crypt_shared_from_url(url, sha)
|
110
|
+
else
|
111
|
+
say_error 'Cannot find download URL for crypt_shared, for this host'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Appends the mongoid gem entries to the Gemfile.
|
116
|
+
def add_mongoid_gem_entries
|
117
|
+
mongoid_gem_entries.each do |group, list|
|
118
|
+
append_to_file 'Gemfile' do
|
119
|
+
prefix = group ? "\ngroup :#{group} do\n" : "\n"
|
120
|
+
suffix = group ? "\nend\n" : "\n"
|
121
|
+
indent_size = group ? 2 : 0
|
122
|
+
|
123
|
+
prefix +
|
124
|
+
list.map { |entry| indent_entry(entry, indent_size) }
|
125
|
+
.join("\n\n") +
|
126
|
+
suffix
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Emit the mongoid.yml file to the new application folder. The
|
132
|
+
# mongoid.yml file is taken directly from the installed mongoid
|
133
|
+
# gem.
|
134
|
+
def mongoid_yml
|
135
|
+
file = Gem.find_files('rails/generators/mongoid/config/templates/mongoid.yml').first
|
136
|
+
database_name = app_name
|
137
|
+
template file, 'config/mongoid.yml', context: binding
|
138
|
+
end
|
139
|
+
|
140
|
+
# Appends a new local master key to the credentials file
|
141
|
+
def add_mongodb_local_master_key_to_credentials
|
142
|
+
return unless @okay_to_support_encryption
|
143
|
+
|
144
|
+
say_status :append, CREDENTIALS_FILE_PATH
|
145
|
+
|
146
|
+
credentials_file.change do |tmp_path|
|
147
|
+
File.open(tmp_path, 'a') do |io|
|
148
|
+
io.puts
|
149
|
+
io.puts '# Master key for MongoDB auto encryption'
|
150
|
+
# passing `96 / 2` because we need a 96-byte key, but
|
151
|
+
# SecureRandom.hex returns a hex-encoded string, which will
|
152
|
+
# be two bytes for requested byte.
|
153
|
+
io.puts "mongodb_master_key: '#{SecureRandom.hex(96 / 2)}'"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# If encryption is enabled, update the mongoid.yml with the necessary
|
159
|
+
# options for encryption.
|
160
|
+
def add_encryption_options_to_mongoid_yml
|
161
|
+
return unless @okay_to_support_encryption
|
162
|
+
|
163
|
+
mongoid_yml = File.join(Dir.pwd, 'config/mongoid.yml')
|
164
|
+
contents = File.read(mongoid_yml)
|
165
|
+
|
166
|
+
contents = insert_key_vault_config(contents)
|
167
|
+
contents = insert_auto_encryption_options(contents)
|
168
|
+
contents = insert_preload_models_option(contents)
|
169
|
+
|
170
|
+
say_status :update, 'config/mongoid.yml'
|
171
|
+
File.write(mongoid_yml, contents)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Emit the mongoid.rb initializer. Unlike mongoid.yml, this is not
|
175
|
+
# taken from the mongoid gem, because mongoid versions prior to 9
|
176
|
+
# did not include an initializer template.
|
177
|
+
def mongoid_initializer
|
178
|
+
template '_config/initializers/mongoid.rb', 'config/initializers/mongoid.rb'
|
179
|
+
end
|
180
|
+
|
181
|
+
# Emit the bin/railsmdb script to the new app's bin folder. The
|
182
|
+
# existing bin/rails script is removed, and replaced by a link to
|
183
|
+
# bin/railsmdb.
|
184
|
+
def railsmdb
|
185
|
+
template '_bin/railsmdb', 'bin/railsmdb' do |content|
|
186
|
+
"#{shebang}\n" + content
|
187
|
+
end
|
188
|
+
|
189
|
+
chmod 'bin/railsmdb', 0o755, verbose: false
|
190
|
+
|
191
|
+
remove_file 'bin/rails', verbose: false
|
192
|
+
create_link 'bin/rails', File.expand_path('bin/railsmdb', destination_root), verbose: false
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
# Returns the Railsmdb project directory if run from inside a
|
198
|
+
# checkout of the railsmdb repository. Otherwise, returns nil.
|
199
|
+
#
|
200
|
+
# @return [ String | nil ] the railsmdb project directory, or nil
|
201
|
+
# if not run within a railsmdb checkout.
|
202
|
+
def railsmdb_project_directory
|
203
|
+
return @railsmdb_project_directory if defined?(@railsmdb_project_directory)
|
204
|
+
|
205
|
+
@railsmdb_project_directory ||= begin
|
206
|
+
path = @initial_path
|
207
|
+
|
208
|
+
while path != '/'
|
209
|
+
break if File.exist?(File.join(path, 'railsmdb.gemspec'))
|
210
|
+
|
211
|
+
path = File.dirname(path)
|
212
|
+
end
|
213
|
+
|
214
|
+
(path == '/') ? nil : path
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Adds indentation to the string representation of the given
|
219
|
+
# gem entry.
|
220
|
+
#
|
221
|
+
# @param [ Rails::Generators::AppBase::GemfileEntry ] entry The
|
222
|
+
# GemfileEntry instance to format.
|
223
|
+
# @param [ Integer ] indent_size the number of spaces to prepend
|
224
|
+
# to each line of the entry's string representation.
|
225
|
+
#
|
226
|
+
# @return [ String ] the string representation of the given entry
|
227
|
+
# with each line indented by the given number of spaces.
|
228
|
+
def indent_entry(entry, indent_size)
|
229
|
+
if indent_size < 1
|
230
|
+
entry.to_s
|
231
|
+
else
|
232
|
+
indent = ' ' * indent_size
|
233
|
+
entry.to_s.gsub(/^/, indent)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# The gem entries to be appended to the Gemfile, sorted by gem
|
238
|
+
# group.
|
239
|
+
#
|
240
|
+
# @return [ Hash ] a hash where the keys are the gem groups, and
|
241
|
+
# the values are lists of gem entries corresponding to those
|
242
|
+
# groups.
|
243
|
+
def mongoid_gem_entries
|
244
|
+
{
|
245
|
+
nil => [
|
246
|
+
mongoid_gem_entry,
|
247
|
+
railsmdb_gem_entry
|
248
|
+
].tap { |list| maybe_add_encryption_gems(list) }
|
249
|
+
}
|
250
|
+
end
|
251
|
+
|
252
|
+
# The gem entry for the Mongoid gem. The version is set to whichever
|
253
|
+
# version is installed and active.
|
254
|
+
#
|
255
|
+
# @return [ Rails::Generators::AppBase::GemfileEntry ] the gem
|
256
|
+
# entry for Mongoid.
|
257
|
+
def mongoid_gem_entry
|
258
|
+
if @okay_to_support_encryption && ::Mongoid::VERSION < '9.0'
|
259
|
+
# FIXME: once Mongoid 9.0 is released, update this so that it
|
260
|
+
# uses that released version.
|
261
|
+
GemfileEntry.github \
|
262
|
+
'mongoid',
|
263
|
+
'mongodb/mongoid',
|
264
|
+
'master',
|
265
|
+
'Encryption requires an unreleased version of Mongoid'
|
266
|
+
else
|
267
|
+
GemfileEntry.version \
|
268
|
+
'mongoid',
|
269
|
+
::Mongoid::VERSION,
|
270
|
+
'Use MongoDB for the database, with Mongoid as the ODM'
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# The gem entry for the Railsmdb gem. If run from a railsmdb
|
275
|
+
# checkout, the gem will reference the path to that checkout.
|
276
|
+
# Otherwise, the version is set to whichever
|
277
|
+
# version is installed and active.
|
278
|
+
#
|
279
|
+
# @return [ Rails::Generators::AppBase::GemfileEntry ] the gem
|
280
|
+
# entry for Railsmdb.
|
281
|
+
def railsmdb_gem_entry
|
282
|
+
if railsmdb_project_directory.present?
|
283
|
+
GemfileEntry.path \
|
284
|
+
'railsmdb',
|
285
|
+
railsmdb_project_directory,
|
286
|
+
'The development version of railsmdb'
|
287
|
+
else
|
288
|
+
GemfileEntry.version \
|
289
|
+
'railsmdb',
|
290
|
+
Railsmdb::Version::STRING,
|
291
|
+
'The Rails CLI tool for MongoDB'
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Build the gem entry for the libmongocrypt-helper gem.
|
296
|
+
#
|
297
|
+
# @return [ Rails::Generators::AppBase::GemfileEntry ] the gem
|
298
|
+
# entry for libmongocrypt-helper gem.
|
299
|
+
def libmongocrypt_helper_gem_entry
|
300
|
+
GemfileEntry.version \
|
301
|
+
'libmongocrypt-helper', '~> 1.8',
|
302
|
+
'Encryption helper for MongoDB-based applications'
|
303
|
+
end
|
304
|
+
|
305
|
+
# Build the gem entry for the ffi gem.
|
306
|
+
#
|
307
|
+
# @return [ Rails::Generators::AppBase::GemfileEntry ] the gem
|
308
|
+
# entry for ffi gem.
|
309
|
+
def ffi_gem_entry
|
310
|
+
GemfileEntry.version \
|
311
|
+
'ffi', nil,
|
312
|
+
'Mongoid needs the ffi gem when encryption is enabled'
|
313
|
+
end
|
314
|
+
|
315
|
+
# If encryption is enabled, adds the necessary gems to the given list
|
316
|
+
# of gem entries, to prepare them to be added to the gemfile.
|
317
|
+
#
|
318
|
+
# @param [ Array<GemfileEntry> ] list The list of gemfile entries.
|
319
|
+
def maybe_add_encryption_gems(list)
|
320
|
+
return unless @okay_to_support_encryption
|
321
|
+
|
322
|
+
list.push libmongocrypt_helper_gem_entry
|
323
|
+
list.push ffi_gem_entry
|
324
|
+
end
|
325
|
+
|
326
|
+
# The location of the directory where the crypt_shared library will
|
327
|
+
# be saved to, relative to the app root.
|
328
|
+
CRYPT_SHARED_DIR = 'vendor/crypt_shared'
|
329
|
+
|
330
|
+
# Download and extract the crypt_shared library from the given url,
|
331
|
+
# and install it in vendor/crypt_shared.
|
332
|
+
#
|
333
|
+
# @param [ String ] url the url to the crypt_shared library archive
|
334
|
+
# @param [ String ] sha the sha hash for the file
|
335
|
+
def fetch_and_extract_crypt_shared_from_url(url, sha)
|
336
|
+
archive = fetch_crypt_shared_from_cache(url, sha) ||
|
337
|
+
fetch_crypt_shared_from_url(url, sha)
|
338
|
+
|
339
|
+
return unless archive
|
340
|
+
|
341
|
+
log :directory, CRYPT_SHARED_DIR
|
342
|
+
FileUtils.mkdir_p CRYPT_SHARED_DIR
|
343
|
+
|
344
|
+
extracted = extract_crypt_shared_from_file(archive)
|
345
|
+
|
346
|
+
log :error, 'No crypt_shared library could be found in the downloaded archive' unless extracted
|
347
|
+
end
|
348
|
+
|
349
|
+
# Computes the path to the cache for the file at the given URL.
|
350
|
+
#
|
351
|
+
# @param [ String ] url the url to consider
|
352
|
+
#
|
353
|
+
# @return [ String ] the path to the file's location on disk.
|
354
|
+
def cached_file_for(url)
|
355
|
+
uri = URI.parse(url)
|
356
|
+
File.join(Dir.tmpdir, File.basename(uri.path))
|
357
|
+
end
|
358
|
+
|
359
|
+
# Look in the cache location for a file downloaded from the given
|
360
|
+
# url. If it exists, make sure the SHA hash matches.
|
361
|
+
#
|
362
|
+
# @param [ String ] url the url to fetch the file from
|
363
|
+
# @param [ String ] sha the sha256 hash for the file
|
364
|
+
#
|
365
|
+
# @return [ String | nil ] the path to the file, if it exists, or nil
|
366
|
+
def fetch_crypt_shared_from_cache(url, sha)
|
367
|
+
path = cached_file_for(url)
|
368
|
+
|
369
|
+
return path if File.exist?(path) && Digest::SHA256.file(path).to_s == sha
|
370
|
+
|
371
|
+
nil
|
372
|
+
end
|
373
|
+
|
374
|
+
# Download the crypt_shared library archive from the given url and
|
375
|
+
# store it in the current directory.
|
376
|
+
#
|
377
|
+
# @param [ String ] url the url to fetch the file from
|
378
|
+
# @param [ String ] sha the sha256 hash for the file
|
379
|
+
#
|
380
|
+
# @return [ String ] the filename that the archive was saved to
|
381
|
+
def fetch_crypt_shared_from_url(url, sha)
|
382
|
+
log :fetch, url
|
383
|
+
|
384
|
+
cached_file_for(url).tap do |archive|
|
385
|
+
Railsmdb::Downloader.fetch(url, archive) { print '.' }
|
386
|
+
puts
|
387
|
+
|
388
|
+
unless File.exist?(archive) && Digest::SHA256.file(archive).to_s == sha
|
389
|
+
log :error, 'an uncorrupted crypt-shared library could not be downloaded'
|
390
|
+
return nil
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Extracts the crypt_shared library from the given archive file, and
|
396
|
+
# writes it to the CRYPT_SHARED_DIR.
|
397
|
+
#
|
398
|
+
# @param [ String ] archive the path to the archive file
|
399
|
+
#
|
400
|
+
# @return [ String | nil ] the name of the extracted file if successful,
|
401
|
+
# or nil if no file could be extracted.
|
402
|
+
def extract_crypt_shared_from_file(archive)
|
403
|
+
extractor = Railsmdb::Extractor.for(archive)
|
404
|
+
extractor.extract(%r{/mongo_crypt_v1\.(so|dylib|dll)}) do |name, data|
|
405
|
+
file = File.join(CRYPT_SHARED_DIR, File.basename(name))
|
406
|
+
|
407
|
+
log :create, file
|
408
|
+
File.open(file, 'w:BINARY') { |io| io.write(data) }
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Ask the user if they agree to the MongoDB Customer Agreement, which is
|
413
|
+
# required in order to download the crypt_shared library.
|
414
|
+
#
|
415
|
+
# @return [ true | false ] whether the user agrees or not
|
416
|
+
def okay_with_legal_shenanigans?
|
417
|
+
# primarily so we can interact with this programmatically in tests...
|
418
|
+
$stdout.sync = true
|
419
|
+
|
420
|
+
say "You've requested to begin a new Rails app with MongoDB encryption."
|
421
|
+
say
|
422
|
+
|
423
|
+
say "Using MongoDB's encryption features requires MongoDB Enterprise Edition,"
|
424
|
+
say 'which is for MongoDB customers only. Are you a MongoDB Atlas customer, or'
|
425
|
+
say 'are you currently a MongoDB Enterprise Advanced subscriber?'
|
426
|
+
say
|
427
|
+
|
428
|
+
case ask('"[yes], I am a MongoDB customer", or "[no], I am not" =>', limited_to: %w[ yes no ])
|
429
|
+
when 'yes'
|
430
|
+
say
|
431
|
+
say '* Use of these features constitutes acceptance of the Customer Agreement.'
|
432
|
+
say
|
433
|
+
|
434
|
+
true
|
435
|
+
when 'no'
|
436
|
+
say
|
437
|
+
say '* Encryption will not be enabled for your application.'
|
438
|
+
say
|
439
|
+
|
440
|
+
false
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Attempts to insert the key-vault configuration into the given
|
445
|
+
# string, which must be the contents of the generated mongoid.yml file.
|
446
|
+
#
|
447
|
+
# @param [String] contents the contents of mongoid.yml
|
448
|
+
#
|
449
|
+
# @return [ String ] the updated contents
|
450
|
+
def insert_key_vault_config(contents)
|
451
|
+
position = (contents =~ /^\s*# Defines the default client/)
|
452
|
+
unless position
|
453
|
+
say_error 'Default mongoid.yml format has changed; cannot update it with key-vault settings'
|
454
|
+
return contents
|
455
|
+
end
|
456
|
+
|
457
|
+
indent_size = contents[position..][/^\s*/].length
|
458
|
+
contents[position, 0] = KEY_VAULT_CONFIG.indent(indent_size).gsub(/%app%/, app_name)
|
459
|
+
|
460
|
+
contents
|
461
|
+
end
|
462
|
+
|
463
|
+
# Returns the path to the downloaded crypt-shared library.
|
464
|
+
#
|
465
|
+
# @return [ String ] path to the crypt_shared library.
|
466
|
+
def crypt_shared_path
|
467
|
+
ext = if OS.windows? || OS::Underlying.windows?
|
468
|
+
'dll'
|
469
|
+
elsif OS.mac?
|
470
|
+
'dylib'
|
471
|
+
else
|
472
|
+
'so'
|
473
|
+
end
|
474
|
+
|
475
|
+
# excuse the final '# >'' at the end of the next line...this string
|
476
|
+
# confuses vscode's syntax highlighter, and that final comment is
|
477
|
+
# to shake it back to its senses...
|
478
|
+
%{"<%= Rails.root.join('vendor', 'crypt_shared', 'mongo_crypt_v1.#{ext}') %>"} # >
|
479
|
+
end
|
480
|
+
|
481
|
+
# Attempts to insert the auto-encryption configuration into the given
|
482
|
+
# string, which must be the contents of the generated mongoid.yml file.
|
483
|
+
#
|
484
|
+
# @param [String] contents the contents of mongoid.yml
|
485
|
+
#
|
486
|
+
# @return [ String ] the updated contents
|
487
|
+
def insert_auto_encryption_options(contents)
|
488
|
+
position = (contents =~ /\sdefault:.*?\s+options:\n/m)
|
489
|
+
|
490
|
+
unless position
|
491
|
+
say_error 'Default mongoid.yml format has changed; cannot update it with auto-encryption settings'
|
492
|
+
return contents
|
493
|
+
end
|
494
|
+
|
495
|
+
position += Regexp.last_match(0).length
|
496
|
+
|
497
|
+
indent_size = contents[position..][/^\s*/].length
|
498
|
+
contents[position, 0] = AUTO_ENCRYPTION_CONFIG
|
499
|
+
.indent(indent_size)
|
500
|
+
.gsub(/%crypt-shared-path%/, crypt_shared_path)
|
501
|
+
|
502
|
+
contents
|
503
|
+
end
|
504
|
+
|
505
|
+
# Attempts to enable the preload_models option in the given
|
506
|
+
# string, which must be the contents of the generated mongoid.yml file.
|
507
|
+
#
|
508
|
+
# @param [String] contents the contents of mongoid.yml
|
509
|
+
#
|
510
|
+
# @return [ String ] the updated contents
|
511
|
+
def insert_preload_models_option(contents)
|
512
|
+
position = (contents =~ /^\s+# preload_models: .*?\n/)
|
513
|
+
|
514
|
+
unless position
|
515
|
+
say_error 'Default mongoid.yml format has changed; cannot enable preload_models'
|
516
|
+
return contents
|
517
|
+
end
|
518
|
+
|
519
|
+
length = Regexp.last_match(0).length
|
520
|
+
|
521
|
+
indent_size = contents[position..][/^\s*/].length
|
522
|
+
contents[position, length] = PRELOAD_MODELS_OPTION.indent(indent_size)
|
523
|
+
|
524
|
+
contents
|
525
|
+
end
|
526
|
+
|
527
|
+
CREDENTIALS_FILE_PATH = 'config/credentials.yml.enc'
|
528
|
+
|
529
|
+
# Return the encrypted credentials file.
|
530
|
+
#
|
531
|
+
# @return [ ActiveSupport::EncryptedConfiguration ] the encrypted
|
532
|
+
# credentials file.
|
533
|
+
def credentials_file
|
534
|
+
ActiveSupport::EncryptedConfiguration.new(
|
535
|
+
config_path: CREDENTIALS_FILE_PATH,
|
536
|
+
key_path: 'config/master.key',
|
537
|
+
env_key: 'RAILS_MASTER_KEY',
|
538
|
+
raise_if_missing_key: true
|
539
|
+
)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
require 'railsmdb/generators/setup/concerns/setuppable'
|
5
|
+
|
6
|
+
module Railsmdb
|
7
|
+
module Generators
|
8
|
+
# The implementation of the setup generator for Railsmdb, for
|
9
|
+
# configuring railsmdb to work in an existing Rails application.
|
10
|
+
class SetupGenerator < Rails::Generators::Base
|
11
|
+
include Railsmdb::Generators::Setup::Concerns::Setuppable
|
12
|
+
|
13
|
+
NEED_RAILS_APP_WARNING = <<~WARNING
|
14
|
+
The `railsmdb setup` command must be run from the root of an
|
15
|
+
existing Rails application. It will add railsmdb to the project,
|
16
|
+
prepare the application to use Mongoid for data access, and
|
17
|
+
replace the `bin/rails` script with `bin/railsmdb`.
|
18
|
+
|
19
|
+
Please try this command again from the root of an existing Rails
|
20
|
+
application.
|
21
|
+
WARNING
|
22
|
+
|
23
|
+
ALREADY_HAS_RAILSMDB = <<~WARNING
|
24
|
+
This Rails application is already configured to use railsmdb.
|
25
|
+
WARNING
|
26
|
+
|
27
|
+
WARN_ABOUT_UNDO = <<~WARNING
|
28
|
+
It is strongly recommended to invoke this in a separate branch,
|
29
|
+
where you can safely test the changes made by the script and
|
30
|
+
roll them back if they cause problems.
|
31
|
+
WARNING
|
32
|
+
|
33
|
+
add_shebang_option!
|
34
|
+
|
35
|
+
# Make sure the current directory is an appropriate place to
|
36
|
+
# run this generator. It must:
|
37
|
+
# - be the root directory of a Rails project
|
38
|
+
# - not already have been set up with railsmdb
|
39
|
+
#
|
40
|
+
# Additionally, this will encourage the user to run this generator
|
41
|
+
# in a branch, in order to safely see what it does to their app.
|
42
|
+
def ensure_proper_invocation
|
43
|
+
ensure_rails_app!
|
44
|
+
ensure_railsmdb_not_already_present!
|
45
|
+
warn_about_undo!
|
46
|
+
end
|
47
|
+
|
48
|
+
public_task :save_initial_path
|
49
|
+
public_task :confirm_legal_shenanigans
|
50
|
+
public_task :fetch_crypt_shared
|
51
|
+
public_task :add_mongoid_gem_entries
|
52
|
+
public_task :mongoid_yml
|
53
|
+
public_task :add_mongodb_local_master_key_to_credentials
|
54
|
+
public_task :add_encryption_options_to_mongoid_yml
|
55
|
+
public_task :mongoid_initializer
|
56
|
+
public_task :railsmdb
|
57
|
+
|
58
|
+
# Make sure the newly required gems are installed automatically.
|
59
|
+
def run_bundle_install
|
60
|
+
say_status :run, 'bundle install'
|
61
|
+
system 'bundle install'
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Infers the name of the app from the application's config/application.rb
|
67
|
+
# file.
|
68
|
+
#
|
69
|
+
# @return [ String ] the name of the application
|
70
|
+
def app_name
|
71
|
+
@app_name ||= File.read('config/application.rb').match(/module (\w+)/)[1].underscore
|
72
|
+
end
|
73
|
+
|
74
|
+
# Warns and exits if the current directory is not the root of a
|
75
|
+
# Rails project.
|
76
|
+
def ensure_rails_app!
|
77
|
+
return if rails_app?
|
78
|
+
|
79
|
+
warn NEED_RAILS_APP_WARNING
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
|
83
|
+
# Warns and exits if railsmdb is already present in the current
|
84
|
+
# Rails project.
|
85
|
+
def ensure_railsmdb_not_already_present!
|
86
|
+
return unless railsmdb?
|
87
|
+
|
88
|
+
warn ALREADY_HAS_RAILSMDB
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
|
92
|
+
# Encourages the user to run this in a branch so that the changes
|
93
|
+
# may be easily rolled back.
|
94
|
+
#
|
95
|
+
# If the user chooses not to proceed, this method will exit the
|
96
|
+
# program.
|
97
|
+
def warn_about_undo!
|
98
|
+
warn WARN_ABOUT_UNDO
|
99
|
+
say
|
100
|
+
|
101
|
+
exit 0 if ask('Do you wish to proceed in the current branch?', limited_to: %w[ yes no ]) == 'no'
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if the current directory appears to be a Rails app.
|
105
|
+
#
|
106
|
+
# @return [ true | false ] if the current directory is a Rails app
|
107
|
+
# or not.
|
108
|
+
def rails_app?
|
109
|
+
File.exist?('bin/rails') &&
|
110
|
+
File.exist?('app') &&
|
111
|
+
File.exist?('config')
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns true if railsmdb appears to already be present in the
|
115
|
+
# current Rails app.
|
116
|
+
#
|
117
|
+
# @return [ true | false ] if railsmdb is already present.
|
118
|
+
def railsmdb?
|
119
|
+
File.exist?('bin/railsmdb')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|