Generate secret_key_base for all new credentials

Currently, when `config/credentials.yml.enc` is generated by
`CredentialsGenerator`, it includes a `secret_key_base` for convenience.
However, because `config/credentials/#{environment}.yml.enc` files are
generated by a different generator (`EncryptedFileGenerator`), they do
not include a `secret_key_base`.

This commit revises `CredentialsGenerator` to be more generator-like,
and changes `rails credentials:edit` to use it for generating both
`config/credentials.yml.enc` and `config/credentials/#{environment}.yml.enc`
files, thereby always including a `secret_key_base`.
This commit is contained in:
Jonathan Hefner 2022-07-06 15:42:42 -05:00
parent 189356bd24
commit 915776ad04
5 changed files with 72 additions and 63 deletions

View File

@ -1,3 +1,9 @@
* Newly generated per-environment credentials files (e.g.
`config/credentials/production.yml.enc`) now include a `secret_key_base` for
convenience, just as `config/credentials.yml.enc` does.
*Jonathan Hefner*
* `--no-*` options now work with the app generator's `--minimal` option, and
are both comprehensive and precise. For example:

View File

@ -82,11 +82,7 @@ module Rails
end
def ensure_credentials_have_been_added
if options[:environment]
encrypted_file_generator.add_encrypted_file_silently(content_path, key_path)
else
credentials_generator.add_credentials_file_silently
end
credentials_generator.add_credentials_file
end
def change_credentials_in_system_editor
@ -122,18 +118,11 @@ module Rails
Rails::Generators::EncryptionKeyFileGenerator.new
end
def encrypted_file_generator
require "rails/generators"
require "rails/generators/rails/encrypted_file/encrypted_file_generator"
Rails::Generators::EncryptedFileGenerator.new
end
def credentials_generator
require "rails/generators"
require "rails/generators/rails/credentials/credentials_generator"
Rails::Generators::CredentialsGenerator.new
Rails::Generators::CredentialsGenerator.new([content_path, key_path], quiet: true)
end
end
end

View File

@ -179,7 +179,7 @@ module Rails
return if options[:pretend] || options[:dummy_app]
require "rails/generators/rails/credentials/credentials_generator"
Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently
Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file
end
def credentials_diff_enroll

View File

@ -7,42 +7,39 @@ require "active_support/encrypted_configuration"
module Rails
module Generators
class CredentialsGenerator < Base # :nodoc:
def add_credentials_file
unless credentials.content_path.exist?
template = credentials_template
argument :content_path, default: "config/credentials.yml.enc"
argument :key_path, default: "config/master.key"
say "Adding #{credentials.content_path} to store encrypted credentials."
def add_credentials_file
in_root do
return if File.exist?(content_path)
say "Adding #{content_path} to store encrypted credentials."
say ""
encrypted_file.write(content)
say "The following content has been encrypted with the Rails master key:"
say ""
say template, :on_green
say content, :on_green
say ""
add_credentials_file_silently(template)
say "You can edit encrypted credentials with `bin/rails credentials:edit`."
say ""
end
end
def add_credentials_file_silently(template = nil)
unless credentials.content_path.exist?
credentials.write(credentials_template)
end
end
private
def credentials
def encrypted_file
ActiveSupport::EncryptedConfiguration.new(
config_path: "config/credentials.yml.enc",
key_path: "config/master.key",
config_path: content_path,
key_path: key_path,
env_key: "RAILS_MASTER_KEY",
raise_if_missing_key: true
)
end
def credentials_template
<<~YAML
def content
@content ||= <<~YAML
# aws:
# access_key_id: 123
# secret_access_key: 345

View File

@ -22,7 +22,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
test "edit credentials" do
# Run twice to ensure credentials can be reread after first edit pass.
2.times do
assert_match(/access_key_id: 123/, run_edit_command)
assert_match DEFAULT_CREDENTIALS_PATTERN, run_edit_command
end
end
@ -36,11 +36,11 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
test "edit command does not overwrite by default if credentials already exists" do
run_edit_command(editor: 'ruby -e "File.write ARGV[0], %(api_key: abc)"')
assert_match(/api_key: abc/, run_show_command)
write_credentials "foo: bar"
output = run_edit_command
run_edit_command
assert_match(/api_key: abc/, run_show_command)
assert_match %r/foo: bar/, output
assert_no_match DEFAULT_CREDENTIALS_PATTERN, output
end
test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
@ -49,26 +49,30 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
FileUtils.rm("config/master.key")
switch_env("RAILS_MASTER_KEY", key) do
assert_match(/access_key_id: 123/, run_edit_command)
assert_match DEFAULT_CREDENTIALS_PATTERN, run_edit_command
assert_not File.exist?("config/master.key")
end
end
end
test "edit command modifies file specified by environment option" do
assert_match(/access_key_id: 123/, run_edit_command(environment: "production"))
Dir.chdir(app_path) do
assert File.exist?("config/credentials/production.key")
assert File.exist?("config/credentials/production.yml.enc")
end
remove_file "config/credentials.yml.enc"
assert_match DEFAULT_CREDENTIALS_PATTERN, run_edit_command(environment: "production")
assert_no_file "config/credentials.yml.enc"
assert_file "config/credentials/production.key"
assert_file "config/credentials/production.yml.enc"
end
test "edit command properly expands environment option" do
assert_match(/access_key_id: 123/, run_edit_command(environment: "prod"))
Dir.chdir(app_path) do
assert File.exist?("config/credentials/production.key")
assert File.exist?("config/credentials/production.yml.enc")
end
remove_file "config/credentials.yml.enc"
assert_match DEFAULT_CREDENTIALS_PATTERN, run_edit_command(environment: "prod")
assert_no_file "config/credentials.yml.enc"
assert_file "config/credentials/production.key"
assert_file "config/credentials/production.yml.enc"
end
test "edit command does not raise when an initializer tries to access non-existent credentials" do
@ -76,21 +80,20 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
Rails.application.credentials.missing_key!
RUBY
assert_match(/access_key_id: 123/, run_edit_command(environment: "qa"))
assert_match DEFAULT_CREDENTIALS_PATTERN, run_edit_command(environment: "qa")
end
test "edit command generates template file when the file does not exist" do
FileUtils.rm("#{app_path}/config/credentials.yml.enc")
run_edit_command
test "edit command generates credentials file when it does not exist" do
remove_file "config/credentials.yml.enc"
output = run_show_command
assert_match(/access_key_id: 123/, output)
assert_match(/secret_key_base/, output)
assert_match DEFAULT_CREDENTIALS_PATTERN, run_edit_command
assert_file "config/credentials.yml.enc"
end
test "show credentials" do
assert_match(/access_key_id: 123/, run_show_command)
assert_match DEFAULT_CREDENTIALS_PATTERN, run_show_command
end
test "show command raises error when require_master_key is specified and key does not exist" do
@ -108,17 +111,15 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
test "show command displays content specified by environment option" do
run_edit_command(environment: "production")
write_credentials "foo: bar", environment: "production"
assert_match(/access_key_id: 123/, run_show_command(environment: "production"))
assert_match %r/foo: bar/, run_show_command(environment: "production")
end
test "show command properly expands environment option" do
run_edit_command(environment: "production")
write_credentials "foo: bar", environment: "production"
output = run_show_command(environment: "prod")
assert_match(/access_key_id: 123/, output)
assert_no_match(/secret_key_base/, output)
assert_match %r/foo: bar/, run_show_command(environment: "prod")
end
@ -213,6 +214,8 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
private
DEFAULT_CREDENTIALS_PATTERN = /access_key_id: 123\n.*secret_key_base: \h{128}\n/m
def run_edit_command(editor: "cat", environment: nil, **options)
switch_env("EDITOR", editor) do
args = environment ? ["--environment", environment] : []
@ -229,4 +232,18 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
args = [path, ("--enroll" if enroll), ("--disenroll" if disenroll)].compact
rails "credentials:diff", args, **options
end
def write_credentials(content, **options)
switch_env("CONTENT", content) do
run_edit_command(editor: %(ruby -e "File.write ARGV[0], ENV['CONTENT']"), **options)
end
end
def assert_file(relative)
assert File.exist?(app_path(relative)), "Expected file #{relative.inspect} to exist, but it does not"
end
def assert_no_file(relative)
assert_not File.exist?(app_path(relative)), "Expected file #{relative.inspect} to not exist, but it does"
end
end