未验证 提交 68479d09 编写于 作者: K Kasper Timm Hansen

Merge branch 'freeletics-manage-multiple-credential-files'

Fixes https://github.com/rails/rails/pull/30940
...@@ -433,10 +433,41 @@ def secret_key_base ...@@ -433,10 +433,41 @@ def secret_key_base
# the Rails master key, which is either taken from ENV["RAILS_MASTER_KEY"] or from loading # the Rails master key, which is either taken from ENV["RAILS_MASTER_KEY"] or from loading
# `config/master.key`. # `config/master.key`.
def credentials def credentials
@credentials ||= ActiveSupport::EncryptedConfiguration.new \ @credentials ||= encrypted("config/credentials.yml.enc")
config_path: Rails.root.join("config/credentials.yml.enc"), end
key_path: Rails.root.join("config/master.key"),
env_key: "RAILS_MASTER_KEY" # Shorthand to decrypt any encrypted configurations or files.
#
# For any file added with `bin/rails encrypted:edit` call `read` to decrypt
# the file with the master key.
# The master key is either stored in `config/master.key` or `ENV["RAILS_MASTER_KEY"]`.
#
# Rails.application.encrypted("config/mystery_man.key").read
# # => "We've met before, haven't we?"
#
# It's also possible to interpret encrypted YAML files with `config`.
#
# Rails.application.encrypted("config/credentials.yml.enc").config
# # => { next_guys_line: "I don't think so. Where was it you think we met?" }
#
# Any top-level configs are also accessible directly on the return value:
#
# Rails.application.encrypted("config/credentials.yml.enc").next_guys_line
# # => "I don't think so. Where was it you think we met?"
#
# The files or configs can also be encrypted with a custom key. To decrypt with
# a key in the `ENV`, use:
#
# Rails.application.encrypted("config/special_tokens.yml.enc", env_key: "SPECIAL_TOKENS")
#
# Or to decrypt with a file, that should be version control ignored, relative to `Rails.root`:
#
# Rails.application.encrypted("config/special_tokens.yml.enc", key_path: "config/special_tokens.key")
def encrypted(path, key_path: "config/master.key", env_key: "RAILS_MASTER_KEY")
ActiveSupport::EncryptedConfiguration.new \
config_path: Rails.root.join(path),
key_path: Rails.root.join(key_path),
env_key: env_key
end end
def to_app #:nodoc: def to_app #:nodoc:
......
require "active_support/encrypted_file"
module Rails
module Command
module Helpers
module Editor
private
def ensure_editor_available(command:)
if ENV["EDITOR"].to_s.empty?
say "No $EDITOR to open file in. Assign one like this:"
say ""
say %(EDITOR="mate --wait" #{command})
say ""
say "For editors that fork and exit immediately, it's important to pass a wait flag,"
say "otherwise the credentials will be saved immediately with no chance to edit."
false
else
true
end
end
def catch_editing_exceptions
yield
rescue Interrupt
say "Aborted changing file: nothing saved."
rescue ActiveSupport::EncryptedFile::MissingKeyError => error
say error.message
end
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require "active_support" require "active_support"
require "rails/command/helpers/editor"
module Rails module Rails
module Command module Command
class CredentialsCommand < Rails::Command::Base # :nodoc: class CredentialsCommand < Rails::Command::Base # :nodoc:
include Helpers::Editor
no_commands do no_commands do
def help def help
say "Usage:\n #{self.class.banner}" say "Usage:\n #{self.class.banner}"
...@@ -16,41 +19,25 @@ def help ...@@ -16,41 +19,25 @@ def help
def edit def edit
require_application_and_environment! require_application_and_environment!
ensure_editor_available || (return) ensure_editor_available(command: "bin/rails credentials:edit") || (return)
ensure_master_key_has_been_added ensure_master_key_has_been_added
ensure_credentials_have_been_added ensure_credentials_have_been_added
change_credentials_in_system_editor catch_editing_exceptions do
change_credentials_in_system_editor
end
say "New credentials encrypted and saved." say "New credentials encrypted and saved."
rescue Interrupt
say "Aborted changing credentials: nothing saved."
rescue ActiveSupport::EncryptedFile::MissingKeyError => error
say error.message
end end
def show def show
require_application_and_environment! require_application_and_environment!
say Rails.application.credentials.read.presence || say Rails.application.credentials.read.presence ||
"No credentials have been added yet. Use bin/rails credentials:edit to change that." "No credentials have been added yet. Use bin/rails credentials:edit to change that."
end end
private private
def ensure_editor_available
if ENV["EDITOR"].to_s.empty?
say "No $EDITOR to open credentials in. Assign one like this:"
say ""
say %(EDITOR="mate --wait" bin/rails credentials:edit)
say ""
say "For editors that fork and exit immediately, it's important to pass a wait flag,"
say "otherwise the credentials will be saved immediately with no chance to edit."
false
else
true
end
end
def ensure_master_key_has_been_added def ensure_master_key_has_been_added
master_key_generator.add_master_key_file master_key_generator.add_master_key_file
master_key_generator.ignore_master_key_file master_key_generator.ignore_master_key_file
......
# frozen_string_literal: true
require "pathname"
require "active_support"
require "rails/command/helpers/editor"
module Rails
module Command
class EncryptedCommand < Rails::Command::Base # :nodoc:
include Helpers::Editor
class_option :key, aliases: "-k", type: :string,
default: "config/master.key", desc: "The Rails.root relative path to the encryption key"
no_commands do
def help
say "Usage:\n #{self.class.banner}"
say ""
end
end
def edit(file_path)
require_application_and_environment!
ensure_editor_available(command: "bin/rails encrypted:edit") || (return)
ensure_encryption_key_has_been_added(options[:key])
ensure_encrypted_file_has_been_added(file_path, options[:key])
catch_editing_exceptions do
change_encrypted_file_in_system_editor(file_path, options[:key])
end
say "File encrypted and saved."
rescue ActiveSupport::MessageEncryptor::InvalidMessage
say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?"
end
def show(file_path)
require_application_and_environment!
say Rails.application.encrypted(file_path, key_path: options[:key]).read.presence ||
"File '#{file_path}' does not exist. Use bin/rails encrypted:edit #{file_path} to change that."
end
private
def ensure_encryption_key_has_been_added(key_path)
encryption_key_file_generator.add_key_file(key_path)
encryption_key_file_generator.ignore_key_file(key_path)
end
def ensure_encrypted_file_has_been_added(file_path, key_path)
encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
end
def change_encrypted_file_in_system_editor(file_path, key_path)
Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path|
system("#{ENV["EDITOR"]} #{tmp_path}")
end
end
def encryption_key_file_generator
require "rails/generators"
require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
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
end
end
end
# frozen_string_literal: true
require "rails/generators/base"
require "active_support/encrypted_file"
module Rails
module Generators
class EncryptedFileGenerator < Base
def add_encrypted_file(file_path, key_path)
unless File.exist?(file_path)
say "Adding #{file_path} to store encrypted content."
say ""
say "The following content has been encrypted with the encryption key:"
say ""
say template, :on_green
say ""
add_encrypted_file_silently(file_path, key_path)
say "You can edit encrypted file with `bin/rails encrypted:edit #{file_path}`."
say ""
end
end
def add_encrypted_file_silently(file_path, key_path, template = encrypted_file_template)
unless File.exist?(file_path)
setup = { content_path: file_path, key_path: key_path, env_key: "RAILS_MASTER_KEY" }
ActiveSupport::EncryptedFile.new(setup).write(template)
end
end
private
def encrypted_file_template
"# aws:\n# access_key_id: 123\n# secret_access_key: 345\n\n"
end
end
end
end
# frozen_string_literal: true
require "pathname"
require "rails/generators/base"
require "active_support/encrypted_file"
module Rails
module Generators
class EncryptionKeyFileGenerator < Base
def add_key_file(key_path)
key_path = Pathname.new(key_path)
unless key_path.exist?
key = ActiveSupport::EncryptedFile.generate_key
log "Adding #{key_path} to store the encryption key: #{key}"
log ""
log "Save this in a password manager your team can access."
log ""
log "If you lose the key, no one, including you, can access anything encrypted with it."
log ""
add_key_file_silently(key_path, key)
log ""
end
end
def add_key_file_silently(key_path, key = nil)
create_file key_path, key || ActiveSupport::EncryptedFile.generate_key
end
def ignore_key_file(key_path, ignore: key_ignore(key_path))
if File.exist?(".gitignore")
unless File.read(".gitignore").include?(ignore)
log "Ignoring #{key_path} so it won't end up in Git history:"
log ""
append_to_file ".gitignore", ignore
log ""
end
else
log "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:"
log ignore, :on_green
log ""
end
end
private
def key_ignore(key_path)
[ "", "/#{key_path}", "" ].join("\n")
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require "rails/generators/base"
require "pathname" require "pathname"
require "rails/generators/base"
require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
require "active_support/encrypted_file" require "active_support/encrypted_file"
module Rails module Rails
...@@ -20,31 +21,24 @@ def add_master_key_file ...@@ -20,31 +21,24 @@ def add_master_key_file
log "If you lose the key, no one, including you, can access anything encrypted with it." log "If you lose the key, no one, including you, can access anything encrypted with it."
log "" log ""
add_master_key_file_silently key add_master_key_file_silently(key)
log "" log ""
end end
end end
def add_master_key_file_silently(key = nil) def add_master_key_file_silently(key = nil)
create_file MASTER_KEY_PATH, key || ActiveSupport::EncryptedFile.generate_key key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key)
end end
def ignore_master_key_file def ignore_master_key_file
if File.exist?(".gitignore") key_file_generator.ignore_key_file(MASTER_KEY_PATH, ignore: key_ignore)
unless File.read(".gitignore").include?(key_ignore)
log "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:"
log ""
append_to_file ".gitignore", key_ignore
log ""
end
else
log "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:"
log key_ignore, :on_green
log ""
end
end end
private private
def key_file_generator
EncryptionKeyFileGenerator.new
end
def key_ignore def key_ignore
[ "", "# Ignore master key for decrypting credentials and more.", "/#{MASTER_KEY_PATH}", "" ].join("\n") [ "", "# Ignore master key for decrypting credentials and more.", "/#{MASTER_KEY_PATH}", "" ].join("\n")
end end
......
...@@ -13,7 +13,10 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase ...@@ -13,7 +13,10 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
teardown { teardown_app } teardown { teardown_app }
test "edit without editor gives hint" do test "edit without editor gives hint" do
assert_match "No $EDITOR to open credentials in", run_edit_command(editor: "") run_edit_command(editor: "").tap do |output|
assert_match "No $EDITOR to open file in", output
assert_match "bin/rails credentials:edit", output
end
end end
test "edit credentials" do test "edit credentials" do
......
# frozen_string_literal: true
require "isolation/abstract_unit"
require "env_helpers"
require "rails/command"
require "rails/commands/encrypted/encrypted_command"
class Rails::Command::EncryptedCommandTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation, EnvHelpers
setup :build_app
teardown :teardown_app
test "edit without editor gives hint" do
run_edit_command("config/tokens.yml.enc", editor: "").tap do |output|
assert_match "No $EDITOR to open file in", output
assert_match "bin/rails encrypted:edit", output
end
end
test "edit encrypted file" do
# Run twice to ensure file can be reread after first edit pass.
2.times do
assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc"))
end
end
test "edit command does not add master key to gitignore when already exist" do
run_edit_command("config/tokens.yml.enc")
Dir.chdir(app_path) do
assert_match "/config/master.key", File.read(".gitignore")
end
end
test "edit encrypts file with custom key" do
run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
Dir.chdir(app_path) do
assert File.exist?("config/tokens.yml.enc")
assert File.exist?("config/tokens.key")
assert_match "/config/tokens.key", File.read(".gitignore")
end
assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc", key: "config/tokens.key"))
end
test "show encrypted file with custom key" do
run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key"))
end
test "won't corrupt encrypted file when passed wrong key" do
run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
assert_match "passed the wrong key",
run_edit_command("config/tokens.yml.enc", allow_failure: true)
assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key"))
end
private
def run_edit_command(file, key: nil, editor: "cat", **options)
switch_env("EDITOR", editor) do
rails "encrypted:edit", prepare_args(file, key), **options
end
end
def run_show_command(file, key: nil)
rails "encrypted:show", prepare_args(file, key)
end
def prepare_args(file, key)
args = [ file ]
args.push("--key", key) if key
args
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册