Skip to content

Commit

Permalink
Use config.credentials.* in credentials commands
Browse files Browse the repository at this point in the history
This commit changes the credentials commands (e.g. `bin/rails
credentials:edit`) to respect `config.credentials.content_path` and
`config.credentials.key_path` when set in `config/application.rb`.

Before this commit:

* Unlike other `bin/rails` commands, `bin/rails credentials:edit`
  ignored `RAILS_ENV`, and would always edit `config/credentials.yml.enc`.

* `bin/rails credentials:edit --environment foo` would create and edit
  `config/credentials/foo.yml.enc`.

* If `config.credentials.content_path` or `config.credentials.key_path`
  was set, `bin/rails credentials:edit` could not be used to edit the
  credentials.  Editing credentials required using `bin/rails
  encrypted:edit path/to/credentials --key path/to/key`.

After this commit:

* `bin/rails credentials:edit` will edit the credentials file that the
  app would load for the current `RAILS_ENV`.

* `bin/rails credentials:edit` respects `config.credentials.content_path`
  and `config.credentials.key_path` when set in `config/application.rb`.
  Using `RAILS_ENV`, environment-specific paths can be set, such as:

    ```ruby
    # config/application.rb
    module MyCoolApp
      class Application < Rails::Application
        config.credentials.content_path = "my_credentials/#{Rails.env}.yml.enc"

        config.credentials.key_path = "path/to/production.key" if Rails.env.production?
      end
    end
    ```

* `bin/rails credentials:edit --environment foo` will create and edit
  `config/credentials/foo.yml.enc` _if_ `config.credentials.content_path`
  has not been set for the `foo` environment.  Ultimately, it will edit
  the credentials file that the app would load for the `foo` environment.

Note that the credentials commands do not run initializers.  Therefore,
they will not, for example, attempt to connect to the database of the
specified environment.

Fixes #37631.
Fixes #41830.
Fixes #46884.
Closes #46904.

Co-authored-by: Alex Ghiculescu <alex@tanda.co>
  • Loading branch information
jonathanhefner and ghiculescu committed Jan 25, 2023
1 parent 2fc46f9 commit 1068945
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 36 deletions.
16 changes: 14 additions & 2 deletions guides/source/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,23 @@ Guide

#### `config.credentials.content_path`

Configures lookup path for encrypted credentials.
The path of the encrypted credentials file.

Defaults to `config/credentials/#{Rails.env}.yml.enc` if it exists, or
`config/credentials.yml.enc` otherwise.

NOTE: In order for the `bin/rails credentials` commands to recognize this value,
it must be set in `config/application.rb`.

#### `config.credentials.key_path`

Configures lookup path for encryption key.
The path of the encrypted credentials key file.

Defaults to `config/credentials/#{Rails.env}.key` if it exists, or
`config/master.key` otherwise.

NOTE: In order for the `bin/rails credentials` commands to recognize this value,
it must be set in `config/application.rb`.

#### `config.debug_exception_response_format`

Expand Down
44 changes: 44 additions & 0 deletions railties/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
* Credentials commands (e.g. `bin/rails credentials:edit`) now respect
`config.credentials.content_path` and `config.credentials.key_path` when set
in `config/application.rb`.

Before:

* `bin/rails credentials:edit` ignored `RAILS_ENV`, and would always edit
`config/credentials.yml.enc`.

* `bin/rails credentials:edit --environment foo` would create and edit
`config/credentials/foo.yml.enc`.

* If `config.credentials.content_path` or `config.credentials.key_path`
was set, `bin/rails credentials:edit` could not be used to edit the
credentials. Editing credentials required using `bin/rails
encrypted:edit path/to/credentials --key path/to/key`.

After:

* `bin/rails credentials:edit` will edit the credentials file that the app
would load for the current `RAILS_ENV`.

* `bin/rails credentials:edit` respects `config.credentials.content_path`
and `config.credentials.key_path` when set in `config/application.rb`.
Using `RAILS_ENV`, environment-specific paths can be set, such as:

```ruby
# config/application.rb
module MyCoolApp
class Application < Rails::Application
config.credentials.content_path = "my_credentials/#{Rails.env}.yml.enc"

config.credentials.key_path = "path/to/production.key" if Rails.env.production?
end
end
```

* `bin/rails credentials:edit --environment foo` will create and edit
`config/credentials/foo.yml.enc` _if_ `config.credentials.content_path`
has not been set for the `foo` environment. Ultimately, it will edit
the credentials file that the app would load for the `foo` environment.

*Jonathan Hefner*

* Add descriptions for non-Rake commands when running `rails -h`.

*Petrik de Heus*
Expand Down
26 changes: 7 additions & 19 deletions railties/lib/rails/application/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ def initialize(*)
@content_security_policy_nonce_directives = 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
@credentials = ActiveSupport::InheritableOptions.new(credentials_defaults)
@disable_sandbox = false
@add_autoload_paths_to_load_path = true
@permissions_policy = nil
Expand Down Expand Up @@ -539,24 +537,14 @@ def respond_to_missing?(symbol, *)
end

private
def default_credentials_content_path
if credentials_available_for_current_env?
root.join("config", "credentials", "#{Rails.env}.yml.enc")
else
root.join("config", "credentials.yml.enc")
end
end
def credentials_defaults
content_path = root.join("config/credentials/#{Rails.env}.yml.enc")
content_path = root.join("config/credentials.yml.enc") if !content_path.exist?

def default_credentials_key_path
if credentials_available_for_current_env?
root.join("config", "credentials", "#{Rails.env}.key")
else
root.join("config", "master.key")
end
end
key_path = root.join("config/credentials/#{Rails.env}.key")
key_path = root.join("config/master.key") if !key_path.exist?

def credentials_available_for_current_env?
File.exist?(root.join("config", "credentials", "#{Rails.env}.yml.enc"))
{ content_path: content_path, key_path: key_path }
end
end
end
Expand Down
46 changes: 31 additions & 15 deletions railties/lib/rails/commands/credentials/credentials_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ class CredentialsCommand < Rails::Command::Base # :nodoc:
require_relative "credentials_command/diffing"
include Diffing

self.environment_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}"
Expand All @@ -26,20 +24,28 @@ def help

desc "edit", "Opens the decrypted credentials in `$EDITOR` for editing"
def edit
extract_environment_option_from_argument(default_environment: nil)
environment_specified = options[:environment].present?
extract_environment_option_from_argument
ENV["RAILS_ENV"] = options[:environment]
require_application!
load_generators

if environment_specified
@content_path = "config/credentials/#{options[:environment]}.yml.enc" unless config.key?(:content_path)
@key_path = "config/credentials/#{options[:environment]}.key" unless config.key?(:key_path)
end

ensure_encryption_key_has_been_added
ensure_credentials_have_been_added
ensure_credentials_have_been_added(environment_specified)
ensure_diffing_driver_is_configured

change_credentials_in_system_editor
end

desc "show", "Shows the decrypted credentials"
def show
extract_environment_option_from_argument(default_environment: nil)
extract_environment_option_from_argument
ENV["RAILS_ENV"] = options[:environment]
require_application!

say credentials.read.presence || missing_credentials_message
Expand All @@ -55,6 +61,7 @@ def show
def diff(content_path = nil)
if @content_path = content_path
extract_environment_option_from_argument(default_environment: extract_environment_from_path(content_path))
ENV["RAILS_ENV"] = options[:environment]
require_application!

say credentials.read.presence || credentials.content_path.read
Expand All @@ -67,6 +74,18 @@ def diff(content_path = nil)
end

private
def config
Rails.application.config.credentials
end

def content_path
@content_path ||= relative_path(config.content_path)
end

def key_path
@key_path ||= relative_path(config.key_path)
end

def credentials
@credentials ||= Rails.application.encrypted(content_path, key_path: key_path)
end
Expand All @@ -81,18 +100,19 @@ def ensure_encryption_key_has_been_added
encryption_key_file_generator.ignore_key_file(key_path)
end

def ensure_credentials_have_been_added
def ensure_credentials_have_been_added(environment_specified)
require "rails/generators/rails/credentials/credentials_generator"

Rails::Generators::CredentialsGenerator.new(
[content_path, key_path],
skip_secret_key_base: %w[development test].include?(options[:environment]),
skip_secret_key_base: environment_specified && %w[development test].include?(options[:environment]),
quiet: true
).invoke_all
end

def change_credentials_in_system_editor
using_system_editor do
say "Editing #{content_path}..."
credentials.change { |tmp_path| system_editor(tmp_path) }
say "File encrypted and saved."
warn_if_credentials_are_invalid
Expand All @@ -113,22 +133,18 @@ def warn_if_credentials_are_invalid

def missing_credentials_message
if !credentials.key?
"Missing '#{key_path}' to decrypt credentials. See `#{executable(:help)}`"
"Missing '#{key_path}' to decrypt credentials. See `#{executable(:help)}`."
else
"File '#{content_path}' does not exist. Use `#{executable(:edit)}` to change that."
end
end

def content_path
@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"
def relative_path(path)
Rails.root.join(path).relative_path_from(Rails.root).to_s
end

def extract_environment_from_path(path)
available_environments.find { |env| path.include? env } if path.end_with?(".yml.enc")
available_environments.find { |env| path.end_with?("#{env}.yml.enc") }
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def secret_key_base
end

def render_template_to_encrypted_file
empty_directory File.dirname(content_path)

content = nil

encrypted_file.change do |tmp_path|
Expand Down
36 changes: 36 additions & 0 deletions railties/test/commands/credentials_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,25 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(encrypted_content, run_diff_command(content_path))
end


test "respects config.credentials.content_path when set in config/application.rb" do
content_path = "my_secrets/credentials.yml.enc"
add_to_config "config.credentials.content_path = #{content_path.inspect}"

assert_credentials_paths content_path, "config/master.key"

assert_credentials_paths content_path, "config/credentials/production.key", environment: "production"
end

test "respects config.credentials.key_path when set in config/application.rb" do
key_path = "my_secrets/master.key"
add_to_config "config.credentials.key_path = #{key_path.inspect}"

assert_credentials_paths "config/credentials.yml.enc", key_path

assert_credentials_paths "config/credentials/production.yml.enc", key_path, environment: "production"
end

private
DEFAULT_CREDENTIALS_PATTERN = /access_key_id: 123\n.*secret_key_base: \h{128}\n/m

Expand All @@ -310,6 +329,23 @@ def write_credentials(content, **options)
end
end

def assert_credentials_paths(content_path, key_path, environment: nil)
content = "foo: #{content_path}"
remove_file content_path
remove_file key_path

assert_match "Editing #{content_path}", write_credentials(content, environment: environment)
assert_file content_path
assert_file key_path

assert_match content, run_show_command(environment: environment)

# Decrypted diffs apply to credentials files in standard locations only.
if %r"config/credentials(?:/.*)?\.yml\.enc$".match?(content_path)
assert_match content, run_diff_command(content_path)
end
end

def read_file(relative)
File.read(app_path(relative))
end
Expand Down

0 comments on commit 1068945

Please sign in to comment.