diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 673b6eac86..aca55fae80 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -116,12 +116,20 @@ *Richard Schneeman* -* Support environment specific credentials file. +* Support environment specific credentials overrides. - 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`. + So any environment will look for `config/credentials/#{Rails.env}.yml.enc` and fall back + to `config/credentials.yml.enc`. + + The encryption key can be in `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key`. + + Environment credentials overrides can be edited with `rails credentials:edit --environment production`. + If no override is setup for the passed environment, it will be created. + + Additionally, the default lookup paths can be overwritten with these configs: + + - `config.credentials.content_path` + - `config.credentials.key_path` *Wojciech Wnętrzak* diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 3595f60bf8..c2403c57a7 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -293,25 +293,25 @@ module Rails 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") + root.join("config", "credentials", "#{Rails.env}.yml.enc") else - File.join(root, "config", "credentials.yml.enc") + root.join("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") + root.join("config", "credentials", "#{Rails.env}.key") else - File.join(root, "config", "master.key") + root.join("config", "master.key") end end + + def credentials_available_for_current_env? + File.exist?(root.join("config", "credentials", "#{Rails.env}.yml.enc")) + end end end end diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE index 6b33d1ab74..d235592f46 100644 --- a/railties/lib/rails/commands/credentials/USAGE +++ b/railties/lib/rails/commands/credentials/USAGE @@ -41,9 +41,18 @@ 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`. +The `credentials` command supports passing an `--environment` option to create an +environment specific override. That override will takes precedence over the +global `config/credentials.yml.enc` file when running in that environment. So: + + rails credentials:edit --environment development + +will create `config/credentials/development.yml.enc` with the corresponding +encryption key in `config/credentials/development.key` if the credentials file +doesn't exist. + +The encryption key can also be put in `ENV["RAILS_MASTER_KEY"]`, which takes +precedence over the file encryption key. + +In addition to that, the default credentials lookup paths can be overriden through +`config.credentials.content_path` and `config.credentials.key_path`. diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb index 4b30d208e0..852cd401d7 100644 --- a/railties/lib/rails/commands/credentials/credentials_command.rb +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -24,13 +24,11 @@ module Rails ensure_editor_available(command: "bin/rails credentials:edit") || (return) - encrypted = Rails.application.encrypted(content_path, key_path: key_path) - - ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil? - ensure_encrypted_file_has_been_added(content_path, key_path) + ensure_encryption_key_has_been_added if credentials.key.nil? + ensure_credentials_have_been_added catch_editing_exceptions do - change_encrypted_file_in_system_editor(content_path, key_path) + change_credentials_in_system_editor end say "File encrypted and saved." @@ -41,12 +39,38 @@ module Rails def show require_application_and_environment! - encrypted = Rails.application.encrypted(content_path, key_path: key_path) - - say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path) + say credentials.read.presence || missing_credentials_message end private + def credentials + Rails.application.encrypted(content_path, key_path: key_path) + end + + def ensure_encryption_key_has_been_added + 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 + encrypted_file_generator.add_encrypted_file_silently(content_path, key_path) + end + + def change_credentials_in_system_editor + credentials.change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + def missing_credentials_message + if credentials.key.nil? + "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" + else + "File '#{content_path}' does not exist. Use `rails credentials:edit` to change that." + end + end + + def content_path options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" end @@ -56,22 +80,6 @@ module Rails 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_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" @@ -85,14 +93,6 @@ module Rails Rails::Generators::EncryptedFileGenerator.new end - - def missing_encrypted_message(key:, key_path:, file_path:) - if key.nil? - "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" - else - "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that." - end - end end end end diff --git a/railties/test/application/credentials_test.rb b/railties/test/application/credentials_test.rb new file mode 100644 index 0000000000..2f6b109b50 --- /dev/null +++ b/railties/test/application/credentials_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" + +class Rails::CredentialsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup :build_app + teardown :teardown_app + + test "reads credentials from environment specific path" do + write_credentials_override(:production) + + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + + test "reads credentials from customized path and key" do + write_credentials_override(:staging) + 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 + + test "reads credentials using environment variable key" do + write_credentials_override(:production, with_key: false) + + switch_env("RAILS_MASTER_KEY", credentials_key) do + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + end + + private + def write_credentials_override(name, with_key: true) + Dir.chdir(app_path) do + Dir.mkdir "config/credentials" + File.write "config/credentials/#{name}.key", credentials_key if with_key + + # secret_key_base: secret + # mystery: revealed + File.write "config/credentials/#{name}.yml.enc", + "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw==" + end + end + + def credentials_key + "2117e775dc2024d4f49ddf3aeb585919" + end +end diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index d6c81c1fe2..432344bccc 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -165,12 +165,12 @@ module ApplicationTests app.config.some_setting = "a_different_setting" assert_equal "a_different_setting", app.config.some_setting, "The configuration's some_setting should be set." - new_config = Rails::Application::Configuration.new("root_of_application") + new_config = Rails::Application::Configuration.new(Pathname.new("root_of_application")) new_config.some_setting = "some_setting_dude" app.config = new_config assert_equal "some_setting_dude", app.config.some_setting, "The configuration's some_setting should have changed." - assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root." + assert_equal "root_of_application", app.config.root.to_s, "The root should have changed to the new config's root." assert_equal new_config, app.config, "The application's config should have changed to the new config." end end diff --git a/railties/test/credentials_test.rb b/railties/test/credentials_test.rb deleted file mode 100644 index 11765b0de5..0000000000 --- a/railties/test/credentials_test.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require "isolation/abstract_unit" -require "env_helpers" - -class Rails::CredentialsTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation, EnvHelpers - - 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 - - test "reads credentials using environment variable key" do - with_credentials do |content, key| - Dir.chdir(app_path) do - Dir.mkdir("config/credentials") - File.write("config/credentials/production.yml.enc", content) - end - - switch_env("RAILS_MASTER_KEY", key) do - app("production") - - assert_equal "revealed", Rails.application.credentials.mystery - end - 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