Skip to content

Fix: Do not override config.credentials when invoking rails credentials commands #11

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

Merged
merged 1 commit into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions lib/rails/app_env/credentials.rb
Original file line number Diff line number Diff line change
@@ -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?
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/rails/app_env/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Rails
module AppEnv
class Error < StandardError; end
end
end
11 changes: 4 additions & 7 deletions lib/rails/app_env/railtie.rb
Original file line number Diff line number Diff line change
@@ -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|
Expand Down
9 changes: 9 additions & 0 deletions lib/rails/rails_ext/credentials_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Rails
module Command
class CredentialsCommand
def config
Rails::AppEnv::Credentials.original
end
end
end
end
179 changes: 179 additions & 0 deletions test/features/credentials_command_test.rb
Original file line number Diff line number Diff line change
@@ -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
30 changes: 27 additions & 3 deletions test/features/file_helpers.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Loading