Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multi environment credentials. #33521

Merged
merged 1 commit into from Sep 19, 2018
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -1,3 +1,12 @@
* Support environment specific credentials file.

For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by
`ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key.
Edit given environment credentials file by command `rails credentials:edit --environment production`.
Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.

*Wojciech Wnętrzak*

* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment.

*Michael C. Nelson*
@@ -438,8 +438,12 @@ def secret_key_base
# Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with
# the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading
# +config/master.key+.
# If specific credentials file exists for current environment, it takes precedence, thus for +production+
# environment look first for +config/credentials/production.yml.enc+ with master key taken
# from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading +config/credentials/production.key+.
# Default behavior can be overwritten by setting +config.credentials.content_path+ and +config.credentials.key_path+.
def credentials
@credentials ||= encrypted("config/credentials.yml.enc")
@credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path)
end

# Shorthand to decrypt any encrypted configurations or files.
@@ -17,7 +17,7 @@ class Configuration < ::Rails::Engine::Configuration
:session_options, :time_zone, :reload_classes_only_on_change,
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
:content_security_policy_nonce_generator, :require_master_key
:content_security_policy_nonce_generator, :require_master_key, :credentials

attr_reader :encoding, :api_only, :loaded_config_version

@@ -60,6 +60,9 @@ def initialize(*)
@content_security_policy_nonce_generator = nil
@require_master_key = false
@loaded_config_version = nil
@credentials = ActiveSupport::OrderedOptions.new
@credentials.content_path = default_credentials_content_path
@credentials.key_path = default_credentials_key_path
end

def load_defaults(target_version)
@@ -273,6 +276,27 @@ def respond_to_missing?(symbol, *)
true
end
end

private
def credentials_available_for_current_env?
File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc")
end

def default_credentials_content_path
if credentials_available_for_current_env?
File.join(root, "config", "credentials", "#{Rails.env}.yml.enc")
else
File.join(root, "config", "credentials.yml.enc")
end
end

def default_credentials_key_path
if credentials_available_for_current_env?
File.join(root, "config", "credentials", "#{Rails.env}.key")
else
File.join(root, "config", "master.key")
end
end
end
end
end
@@ -38,3 +38,12 @@ the encrypted credentials.
When the temporary file is next saved the contents are encrypted and written to
`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials
from leaking.

=== Environment Specific Credentials

It is possible to have credentials for each environment. If the file for current environment exists it will take
precedence over `config/credentials.yml.enc`, thus for `production` environment first look for
`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]`
or stored in `config/credentials/production.key`.
To edit given file use command `rails credentials:edit --environment production`
Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
@@ -8,6 +8,9 @@ module Command
class CredentialsCommand < Rails::Command::Base # :nodoc:
include Helpers::Editor

class_option :environment, aliases: "-e", type: :string,
desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"

no_commands do
def help
say "Usage:\n #{self.class.banner}"
@@ -20,58 +23,78 @@ def edit
require_application_and_environment!

ensure_editor_available(command: "bin/rails credentials:edit") || (return)
ensure_master_key_has_been_added if Rails.application.credentials.key.nil?
ensure_credentials_have_been_added

encrypted = Rails.application.encrypted(content_path, key_path: key_path, env_key: env_key)

ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil?
ensure_encrypted_file_has_been_added(content_path, key_path)

catch_editing_exceptions do
change_credentials_in_system_editor
change_encrypted_file_in_system_editor(content_path, key_path, env_key)
end

say "New credentials encrypted and saved."
say "File encrypted and saved."
rescue ActiveSupport::MessageEncryptor::InvalidMessage
say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?"
end

def show
require_application_and_environment!

say Rails.application.credentials.read.presence || missing_credentials_message
encrypted = Rails.application.encrypted(content_path, key_path: key_path, env_key: env_key)

say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path)
end

private
def ensure_master_key_has_been_added
master_key_generator.add_master_key_file
master_key_generator.ignore_master_key_file
def content_path
options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
end

def key_path
options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
end

def env_key
options[:environment] ? "RAILS_#{options[:environment].upcase}_KEY" : "RAILS_MASTER_KEY"
end


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_credentials_have_been_added
credentials_generator.add_credentials_file_silently
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_credentials_in_system_editor
Rails.application.credentials.change do |tmp_path|
def change_encrypted_file_in_system_editor(file_path, key_path, env_key)
Rails.application.encrypted(file_path, key_path: key_path, env_key: env_key).change do |tmp_path|
system("#{ENV["EDITOR"]} #{tmp_path}")
end
end


def master_key_generator
def encryption_key_file_generator
require "rails/generators"
require "rails/generators/rails/master_key/master_key_generator"
require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"

Rails::Generators::MasterKeyGenerator.new
Rails::Generators::EncryptionKeyFileGenerator.new
end

def credentials_generator
def encrypted_file_generator
require "rails/generators"
require "rails/generators/rails/credentials/credentials_generator"
require "rails/generators/rails/encrypted_file/encrypted_file_generator"

Rails::Generators::CredentialsGenerator.new
Rails::Generators::EncryptedFileGenerator.new
end

def missing_credentials_message
if Rails.application.credentials.key.nil?
"Missing master key to decrypt credentials. See `rails credentials:help`"
def missing_encrypted_message(key:, key_path:, file_path:)
if key.nil?
"Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
else
"No credentials have been added yet. Use `rails credentials:edit` to change that."
"File '#{file_path}' does not exist. Use `rails credentials:edit` to change that."
end
end
end
@@ -55,6 +55,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
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
end

test "show credentials" do
assert_match(/access_key_id: 123/, run_show_command)
end
@@ -70,17 +78,25 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
remove_file "config/master.key"
add_to_config "config.require_master_key = false"

assert_match(/Missing master key to decrypt credentials/, run_show_command)
assert_match(/Missing 'config\/master\.key' to decrypt credentials/, run_show_command)
end

test "show command displays content specified by environment option" do
run_edit_command(environment: "production")

assert_match(/access_key_id: 123/, run_show_command(environment: "production"))
end

private
def run_edit_command(editor: "cat")
def run_edit_command(editor: "cat", environment: nil, **options)
switch_env("EDITOR", editor) do
rails "credentials:edit"
args = environment ? ["--environment", environment] : []
rails "credentials:edit", args, **options
end
end

def run_show_command(**options)
rails "credentials:show", **options
def run_show_command(environment: nil, **options)
args = environment ? ["--environment", environment] : []
rails "credentials:show", args, **options
end
end
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require "isolation/abstract_unit"

class Rails::CredentialsTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation

setup :build_app
teardown :teardown_app

test "reads credentials from environment specific path" do
with_credentials do |content, key|
Dir.chdir(app_path) do
Dir.mkdir("config/credentials")
File.write("config/credentials/production.yml.enc", content)
File.write("config/credentials/production.key", key)
end

app("production")

assert_equal "revealed", Rails.application.credentials.mystery
end
end

test "reads credentials from customized path and key" do
with_credentials do |content, key|
Dir.chdir(app_path) do
Dir.mkdir("config/credentials")
File.write("config/credentials/staging.yml.enc", content)
File.write("config/credentials/staging.key", key)
end

add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')")
add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')")
app("production")

assert_equal "revealed", Rails.application.credentials.mystery
end
end

private
def with_credentials
key = "2117e775dc2024d4f49ddf3aeb585919"
# secret_key_base: secret
# mystery: revealed
content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw=="
yield(content, key)
end
end
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.