diff --git a/README.md b/README.md index a29229c..60812df 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,16 @@ In case `config/credentials/#{Rails.app_env}.key` does not exist, it falls back As with default Rails behaviours, if `ENV["RAILS_MASTER_KEY"]` is present, it takes precedence over `config/credentials/#{Rails.app_env}.key` and `config/master.key`. +As with default Rails behaviours, when invoking `$ rails credentials` commands, specific the `--environment` option +instead of using `APP_ENV` and `RAILS_ENV`. + +```console +# APP_ENV and RAILS_ENV are ignored. +$ APP_ENV=foo RAILS_ENV=bar bin/rails credentials:edit --environment qaz + create config/credentials/qaz.key + create config/credentials/qaz.yml.enc +``` + Learn more in the [Heroku](#heroku) section below. ### Console diff --git a/lib/rails/app_env/credentials.rb b/lib/rails/app_env/credentials.rb index 95d8713..9eb4776 100644 --- a/lib/rails/app_env/credentials.rb +++ b/lib/rails/app_env/credentials.rb @@ -1,7 +1,32 @@ +require_relative "error" + module Rails module AppEnv module Credentials + class AlreadyInitializedError < Rails::AppEnv::Error; end + class << self + attr_reader :original + + def initialize! + raise AlreadyInitializedError.new "Rails::AppEnv::Credentials has already been initialized." if @initialized + @initialized = true + + @original = Rails.application.config.credentials + Rails.application.config.credentials = configuration + + monkey_patch_rails_credentials_command! + end + + def configuration + ActiveSupport::InheritableOptions.new( + content_path: content_path, + key_path: key_path + ) + end + + private + def content_path path = Rails.root.join("config/credentials/#{Rails.app_env}.yml.enc") path = Rails.root.join("config/credentials.yml.enc") unless path.exist? @@ -13,6 +38,10 @@ def key_path path = Rails.root.join("config/master.key") unless path.exist? path end + + def monkey_patch_rails_credentials_command! + require_relative "../rails_ext/credentials_command" + end end end end diff --git a/lib/rails/app_env/error.rb b/lib/rails/app_env/error.rb new file mode 100644 index 0000000..8df4e65 --- /dev/null +++ b/lib/rails/app_env/error.rb @@ -0,0 +1,5 @@ +module Rails + module AppEnv + class Error < StandardError; end + end +end diff --git a/lib/rails/app_env/railtie.rb b/lib/rails/app_env/railtie.rb index 9dbd338..7579d76 100644 --- a/lib/rails/app_env/railtie.rb +++ b/lib/rails/app_env/railtie.rb @@ -1,19 +1,16 @@ module Rails module AppEnv class Railtie < Rails::Railtie - config.before_configuration do + initializer :load_helpers, before: :initialize_logger do Rails.extend(Helpers) end - config.before_configuration do |app| - app.config.credentials.content_path = Rails::AppEnv::Credentials.content_path - app.config.credentials.key_path = Rails::AppEnv::Credentials.key_path + initializer :set_credentials, before: :initialize_logger do + Rails::AppEnv::Credentials.initialize! end config.after_initialize do - Rails::Info.property "Application environment" do - Rails.app_env - end + Rails::Info.property "Application environment", Rails.app_env end console do |app| diff --git a/lib/rails/rails_ext/credentials_command.rb b/lib/rails/rails_ext/credentials_command.rb new file mode 100644 index 0000000..3878f7e --- /dev/null +++ b/lib/rails/rails_ext/credentials_command.rb @@ -0,0 +1,9 @@ +module Rails + module Command + class CredentialsCommand + def config + Rails::AppEnv::Credentials.original + end + end + end +end diff --git a/test/features/credentials_command_test.rb b/test/features/credentials_command_test.rb new file mode 100644 index 0000000..316fe8a --- /dev/null +++ b/test/features/credentials_command_test.rb @@ -0,0 +1,179 @@ +require_relative "../test_helper" +require_relative "file_helpers" + +module Rails::AppEnv::FeaturesTest + class CredentialsCommandTest < ActiveSupport::TestCase + include FileHelpers + + def teardown + cleanup_credentials_files + end + + test "does not override when --environment is custom" do + arg_env = "foo" + + ["foo", "production", "development", "test", nil].each do |app_env| + ["foo", "production", "development", "test", nil].each do |rails_env| + cleanup_credentials_files + + run_edit_command(app_env:, rails_env:, arg_env:) + + refute_files %w[ + config/credentials.yml.enc + config/master.key + config/credentials/production.yml.enc + config/credentials/production.key + config/credentials/development.yml.enc + config/credentials/development.key + config/credentials/test.yml.enc + config/credentials/test.key + config/credentials/.yml.enc + config/credentials/.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + + assert_files %w[ + config/credentials/foo.yml.enc + config/credentials/foo.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + end + end + end + + test "does not override when --environment is production" do + arg_env = "production" + + ["foo", "production", "development", "test", nil].each do |app_env| + ["foo", "production", "development", "test", nil].each do |rails_env| + cleanup_credentials_files + + run_edit_command(app_env:, rails_env:, arg_env:) + + refute_files %w[ + config/credentials.yml.enc + config/master.key + config/credentials/foo.yml.enc + config/credentials/foo.key + config/credentials/development.yml.enc + config/credentials/development.key + config/credentials/test.yml.enc + config/credentials/test.key + config/credentials/.yml.enc + config/credentials/.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + + assert_files %w[ + config/credentials/production.yml.enc + config/credentials/production.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + end + end + end + + test "does not override when --environment is development" do + arg_env = "development" + + ["foo", "production", "development", "test", nil].each do |app_env| + ["foo", "production", "development", "test", nil].each do |rails_env| + cleanup_credentials_files + + run_edit_command(app_env:, rails_env:, arg_env:) + + refute_files %w[ + config/credentials.yml.enc + config/master.key + config/credentials/foo.yml.enc + config/credentials/foo.key + config/credentials/production.yml.enc + config/credentials/production.key + config/credentials/test.yml.enc + config/credentials/test.key + config/credentials/.yml.enc + config/credentials/.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + + assert_files %w[ + config/credentials/development.yml.enc + config/credentials/development.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + end + end + end + + test "does not override when --environment is test" do + arg_env = "test" + + ["foo", "production", "development", "test", nil].each do |app_env| + ["foo", "production", "development", "test", nil].each do |rails_env| + cleanup_credentials_files + + run_edit_command(app_env:, rails_env:, arg_env:) + + refute_files %W[ + config/credentials.yml.enc + config/master.key + config/credentials/foo.yml.enc + config/credentials/foo.key + config/credentials/production.yml.enc + config/credentials/production.key + config/credentials/development.yml.enc + config/credentials/development.key + config/credentials/.yml.enc + config/credentials/.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + + assert_files %w[ + config/credentials/test.yml.enc + config/credentials/test.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + end + end + end + + test "does not override when --environment are blank" do + arg_env = nil + + ["foo", "production", "development", "test", nil].each do |app_env| + ["foo", "production", "development", "test", nil].each do |rails_env| + cleanup_credentials_files + + run_edit_command(app_env:, rails_env:, arg_env:) + + refute_files %w[ + config/credentials/foo.yml.enc + config/credentials/foo.key + config/credentials/production.yml.enc + config/credentials/production.key + config/credentials/development.yml.enc + config/credentials/development.key + config/credentials/test.yml.enc + config/credentials/test.key + config/credentials/.yml.enc + config/credentials/.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + + assert_files %w[ + config/credentials.yml.enc + config/master.key + ], "when APP_ENV is #{app_env.inspect} and RAILS_ENV is #{rails_env.inspect} and arg_env is #{arg_env.inspect}" + end + end + end + + private + + def run_edit_command(arg_env: nil, app_env: nil, rails_env: nil) + env = {"VISUAL" => "cat", "EDITOR" => "cat", "APP_ENV" => app_env, "RAILS_ENV" => rails_env} + args = arg_env ? ["--environment", arg_env] : [] + + _, status = Open3.capture2(env, "bin/rails", "credentials:edit", *args, {chdir: DUMMY_ROOT}) + + assert_predicate status, :success? + end + + def cleanup_credentials_files + FileUtils.remove_dir dummy_path("config/credentials"), true + FileUtils.remove_file dummy_path("config/credentials.yml.enc"), true + FileUtils.remove_file dummy_path("config/master.key"), true + end + end +end diff --git a/test/features/file_helpers.rb b/test/features/file_helpers.rb index a46ea38..53779df 100644 --- a/test/features/file_helpers.rb +++ b/test/features/file_helpers.rb @@ -1,10 +1,10 @@ module FileHelpers private - def with_file(path, &block) - return block.call nil if path.nil? + def with_file(relative, &block) + return block.call nil if relative.nil? - full_path = Rails.root.join path + full_path = dummy_path(relative) FileUtils.mkdir_p File.dirname(full_path) FileUtils.touch full_path @@ -13,4 +13,28 @@ def with_file(path, &block) ensure FileUtils.rm_f full_path unless full_path.nil? end + + def assert_files(relatives, message = "") + relatives.each do |relative| + assert_file relative, message + end + end + + def refute_files(relatives, message = "") + relatives.each do |relative| + refute_file relative, message + end + end + + def assert_file(relative, message = "") + assert File.exist?(dummy_path(relative)), ["Expected file #{relative.inspect} to exist, but it does", message.strip].join(" ").strip + end + + def refute_file(relative, message = "") + refute File.exist?(dummy_path(relative)), ["Expected file #{relative.inspect} to not exist, but it does", message.strip].join(" ").strip + end + + def dummy_path(relative) + File.expand_path(relative, DUMMY_ROOT) + end end diff --git a/test/units/app_env/credentials_test.rb b/test/units/app_env/credentials_test.rb index ed1d787..583a9ea 100644 --- a/test/units/app_env/credentials_test.rb +++ b/test/units/app_env/credentials_test.rb @@ -2,57 +2,90 @@ require_relative "../../test_helper" class Rails::AppEnv::CredentialsTest < ActiveSupport::TestCase - test "Credentials#content_path returns 'config/credentials/{APP_ENV}.yml.enc' if the file exist" do + test "#initialize! can only be invoked once" do + reset_credentials + + Rails::AppEnv::Credentials.initialize! + + assert_raises Rails::AppEnv::Credentials::AlreadyInitializedError do + Rails::AppEnv::Credentials.initialize! + end + end + + test "#initialize! backup original Rails.application.config.credentials" do + reset_credentials + + expected = Object.new + + Rails.application.config.stub :credentials, expected do + Rails::AppEnv::Credentials.initialize! + end + + assert_same expected, Rails::AppEnv::Credentials.original + end + + test "#configuration is a kind of ActiveSupport::Credentials" do + assert_kind_of ActiveSupport::InheritableOptions, Rails::AppEnv::Credentials.configuration + end + + test "#configuration.content_path is config/credentials/{Rails.app_env}.yml.enc" do Dir.mktmpdir do |tmp_dir| Rails.stub :root, Pathname(tmp_dir) do - Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("fake_foo") do - path = Rails.root.join("config/credentials/fake_foo.yml.enc") + Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("foo") do + path = Rails.root.join("config/credentials/foo.yml.enc") FileUtils.mkdir_p File.dirname(path) FileUtils.touch path - assert_equal path, Rails::AppEnv::Credentials.content_path + assert_equal path, Rails::AppEnv::Credentials.configuration.content_path end end end end - test "Credentials#content_path falls back to 'config/credentials.yml.enc' if the file does not exist" do + test "#configuration.content_path falls back to config/credentials.yml.enc when config/credentials/{Rails.app_env}.yml.enc not exist" do Dir.mktmpdir do |tmp_dir| Rails.stub :root, Pathname(tmp_dir) do - Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("fake_foo") do + Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("foo") do path = Rails.root.join("config/credentials.yml.enc") - assert_equal path, Rails::AppEnv::Credentials.content_path + assert_equal path, Rails::AppEnv::Credentials.configuration.content_path end end end end - test "Credentials#key_path returns 'config/credentials/{APP_ENV}.key' if the file exist" do + test "#configuration.key_path is config/credentials/{APP_ENV}.key" do Dir.mktmpdir do |tmp_dir| Rails.stub :root, Pathname(tmp_dir) do - Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("fake_foo") do - path = Rails.root.join("config/credentials/fake_foo.key") + Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("foo") do + path = Rails.root.join("config/credentials/foo.key") FileUtils.mkdir_p File.dirname(path) FileUtils.touch path - assert_equal path, Rails::AppEnv::Credentials.key_path + assert_equal path, Rails::AppEnv::Credentials.configuration.key_path end end end end - test "Credentials#key_path falls back to 'config/master.key' if the file does not exist" do + test "#configuration.key_path falls back to config/master.key when config/credentials/{APP_ENV}.key not exist" do Dir.mktmpdir do |tmp_dir| Rails.stub :root, Pathname(tmp_dir) do - Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("fake_foo") do + Rails.stub :app_env, Rails::AppEnv::EnvironmentInquirer.new("foo") do path = Rails.root.join("config/master.key") - assert_equal path, Rails::AppEnv::Credentials.key_path + assert_equal path, Rails::AppEnv::Credentials.configuration.key_path end end end end + + private + + def reset_credentials + Rails::AppEnv::Credentials.instance_variable_set :@initialized, nil + Rails::AppEnv::Credentials.instance_variable_set :@original, nil + end end