railsmdb 1.0.0.alpha1
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 +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
|