mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Prettify diff generated by git for encripted file:
- @sinsoku had the idea and started implementing it few months ago but sadly didn't finish it. This PR is taking over his work. The credentials feature has changed a lot since @sinsoku opened hi PR, it was easier to just restart from scratch instead of checking out his branch. Sinsoku will get all the credit he deserves for this idea :) TL;DR on that that feature is to make the `git diff` or `git log` of encrypted files to be readable. The previous implementation was only setting up the git required configuration for the first time Rails was bootstraped, so I decided to instead provide the user a choice to opt-in for readable diff credential whenever a user types the `bin/rails credentials:edit` command. The question won't be asked in the future the user has already answered or if the user already opted in. Co-authored-by: Takumi Shotoku <insoku.listy@gmail.com>
This commit is contained in:
parent
ec7aa03c98
commit
5a4acf7ac4
4 changed files with 185 additions and 9 deletions
55
railties/lib/rails/command/helpers/pretty_credentials.rb
Normal file
55
railties/lib/rails/command/helpers/pretty_credentials.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "fileutils"
|
||||
|
||||
module Rails
|
||||
module Command
|
||||
module Helpers
|
||||
module PrettyCredentials
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
def opt_in_pretty_credentials
|
||||
unless already_answered? || already_opted_in?
|
||||
answer = yes?("Would you like to make the credentials diff from git more readable in the future? [Y/n]")
|
||||
end
|
||||
|
||||
opt_in! if answer
|
||||
FileUtils.touch(tracker) unless answer.nil?
|
||||
rescue Error
|
||||
say("Couldn't setup git to prettify the credentials diff")
|
||||
end
|
||||
|
||||
private
|
||||
def already_answered?
|
||||
tracker.exist?
|
||||
end
|
||||
|
||||
def already_opted_in?
|
||||
system_call("git config --get 'diff.rails_credentials.textconv'", accepted_codes: [0, 1])
|
||||
end
|
||||
|
||||
def opt_in!
|
||||
system_call("git config diff.rails_credentials.textconv 'bin/rails credentials:show'", accepted_codes: [0])
|
||||
|
||||
git_attributes = Rails.root.join(".gitattributes")
|
||||
File.open(git_attributes, "a+") do |file|
|
||||
file.write(<<~EOM)
|
||||
config/credentials/*.yml.enc diff=rails_credentials
|
||||
config/credentials.yml.enc diff=rails_credentials
|
||||
EOM
|
||||
end
|
||||
end
|
||||
|
||||
def tracker
|
||||
Rails.root.join("tmp", "rails_pretty_credentials")
|
||||
end
|
||||
|
||||
def system_call(command_line, accepted_codes:)
|
||||
result = system(command_line)
|
||||
raise(Error) if accepted_codes.exclude?($?.exitstatus)
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
require "active_support"
|
||||
require "rails/command/helpers/editor"
|
||||
require "rails/command/helpers/pretty_credentials"
|
||||
require "rails/command/environment_argument"
|
||||
require "pathname"
|
||||
|
||||
module Rails
|
||||
module Command
|
||||
class CredentialsCommand < Rails::Command::Base # :nodoc:
|
||||
include Helpers::Editor
|
||||
include Helpers::PrettyCredentials
|
||||
include EnvironmentArgument
|
||||
|
||||
self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
|
||||
|
@ -34,20 +37,29 @@ module Rails
|
|||
end
|
||||
|
||||
say "File encrypted and saved."
|
||||
opt_in_pretty_credentials
|
||||
rescue ActiveSupport::MessageEncryptor::InvalidMessage
|
||||
say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?"
|
||||
end
|
||||
|
||||
def show
|
||||
extract_environment_option_from_argument(default_environment: nil)
|
||||
def show(git_textconv_path = nil)
|
||||
if git_textconv_path
|
||||
default_environment = extract_environment_from_path(git_textconv_path)
|
||||
fallback_message = File.read(git_textconv_path)
|
||||
end
|
||||
|
||||
extract_environment_option_from_argument(default_environment: default_environment)
|
||||
require_application!
|
||||
|
||||
say credentials.read.presence || missing_credentials_message
|
||||
say credentials(git_textconv_path).read.presence || fallback_message || missing_credentials_message
|
||||
rescue => e
|
||||
raise(e) unless git_textconv_path
|
||||
fallback_message
|
||||
end
|
||||
|
||||
private
|
||||
def credentials
|
||||
Rails.application.encrypted(content_path, key_path: key_path)
|
||||
def credentials(content = nil)
|
||||
Rails.application.encrypted(content || content_path, key_path: key_path)
|
||||
end
|
||||
|
||||
def ensure_encryption_key_has_been_added
|
||||
|
@ -77,7 +89,6 @@ module Rails
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
def content_path
|
||||
options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
|
||||
end
|
||||
|
@ -86,6 +97,17 @@ module Rails
|
|||
options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
|
||||
end
|
||||
|
||||
def extract_environment_from_path(path)
|
||||
regex = %r{
|
||||
([A-Za-z0-9]+) # match the environment
|
||||
(?<!credentials) # don't match if file contains the word "credentials"
|
||||
# in such case, the environment should be the default one
|
||||
\.yml\.enc # look for `.yml.enc` file extension
|
||||
}x
|
||||
path.match(regex)
|
||||
|
||||
Regexp.last_match(1)
|
||||
end
|
||||
|
||||
def encryption_key_file_generator
|
||||
require "rails/generators"
|
||||
|
|
|
@ -4,6 +4,7 @@ require "isolation/abstract_unit"
|
|||
require "env_helpers"
|
||||
require "rails/command"
|
||||
require "rails/commands/credentials/credentials_command"
|
||||
require "fileutils"
|
||||
|
||||
class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
|
||||
include ActiveSupport::Testing::Isolation, EnvHelpers
|
||||
|
@ -88,10 +89,107 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
|
|||
assert_match(/secret_key_base/, output)
|
||||
end
|
||||
|
||||
test "edit ask the user to opt in to pretty credentials" do
|
||||
assert_match(/Would you like to make the credentials diff from git/, run_edit_command)
|
||||
end
|
||||
|
||||
test "edit doesn't ask the user to opt in to pretty credentials when alreasy asked" do
|
||||
app_file("tmp/rails_pretty_credentials", "")
|
||||
|
||||
assert_no_match(/Would you like to make the credentials diff from git/, run_edit_command)
|
||||
end
|
||||
|
||||
test "edit doesn't ask the user to opt in when user already opted in" do
|
||||
content = <<~EOM
|
||||
[diff "rails_credentials"]
|
||||
textconv = bin/rails credentials:show
|
||||
EOM
|
||||
app_file(".git/config", content)
|
||||
|
||||
assert_no_match(/Would you like to make the credentials diff from git/, run_edit_command)
|
||||
end
|
||||
|
||||
test "edit ask the user to opt in to pretty credentials, user accepts" do
|
||||
file = File.open("foo", "w")
|
||||
file.write("y")
|
||||
file.rewind
|
||||
|
||||
run_edit_command(stdin: file.path)
|
||||
|
||||
git_attributes = app_path(".gitattributes")
|
||||
expected = <<~EOM
|
||||
config/credentials/*.yml.enc diff=rails_credentials
|
||||
config/credentials.yml.enc diff=rails_credentials
|
||||
EOM
|
||||
assert(File.exist?(git_attributes))
|
||||
assert_equal(expected, File.read(git_attributes))
|
||||
Dir.chdir(app_path) do
|
||||
assert_equal("bin/rails credentials:show\n", `git config --get 'diff.rails_credentials.textconv'`)
|
||||
end
|
||||
ensure
|
||||
File.delete(file)
|
||||
end
|
||||
|
||||
test "edit ask the user to opt in to pretty credentials, user refuses" do
|
||||
file = File.open("foo", "w")
|
||||
file.write("n")
|
||||
file.rewind
|
||||
|
||||
run_edit_command(stdin: file.path)
|
||||
|
||||
git_attributes = app_path(".gitattributes")
|
||||
assert_not(File.exist?(git_attributes))
|
||||
ensure
|
||||
File.delete(file)
|
||||
end
|
||||
|
||||
test "show credentials" do
|
||||
assert_match(/access_key_id: 123/, run_show_command)
|
||||
end
|
||||
|
||||
test "show command when argument is provided (from git diff left file)" do
|
||||
run_edit_command(environment: "development")
|
||||
|
||||
assert_match(/access_key_id: 123/, run_show_command("config/credentials/development.yml.enc"))
|
||||
end
|
||||
|
||||
test "show command when argument is provided (from git diff right file)" do
|
||||
run_edit_command(environment: "development")
|
||||
|
||||
dir = Dir.mktmpdir
|
||||
file_path = File.join(dir, "KnAM4a_development.yml.enc")
|
||||
file_content = File.read(app_path("config", "credentials", "development.yml.enc"))
|
||||
File.write(file_path, file_content)
|
||||
|
||||
assert_match(/access_key_id: 123/, run_show_command(file_path))
|
||||
ensure
|
||||
FileUtils.rm_rf(dir)
|
||||
end
|
||||
|
||||
test "show command when argument is provided (git diff) and filename is the master credentials" do
|
||||
assert_match(/access_key_id: 123/, run_show_command("config/credentials.yml.enc"))
|
||||
end
|
||||
|
||||
test "show command when argument is provided (git diff) and master key is not available" do
|
||||
remove_file "config/master.key"
|
||||
|
||||
raw_content = File.read(app_path("config", "credentials.yml.enc"))
|
||||
assert_match(raw_content, run_show_command("config/credentials.yml.enc"))
|
||||
end
|
||||
|
||||
test "show command when argument is provided (git diff) return the raw encrypted content in an error occurs" do
|
||||
run_edit_command(environment: "development")
|
||||
|
||||
dir = Dir.mktmpdir
|
||||
file_path = File.join(dir, "20190807development.yml.enc")
|
||||
file_content = File.read(app_path("config", "credentials", "development.yml.enc"))
|
||||
File.write(file_path, file_content)
|
||||
|
||||
assert_match(file_content, run_show_command(file_path))
|
||||
ensure
|
||||
FileUtils.rm_rf(dir)
|
||||
end
|
||||
|
||||
test "show command raises error when require_master_key is specified and key does not exist" do
|
||||
remove_file "config/master.key"
|
||||
add_to_config "config.require_master_key = true"
|
||||
|
@ -128,8 +226,9 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def run_show_command(environment: nil, **options)
|
||||
def run_show_command(path = nil, environment: nil, **options)
|
||||
args = environment ? ["--environment", environment] : []
|
||||
args.unshift(path)
|
||||
rails "credentials:show", args, **options
|
||||
end
|
||||
end
|
||||
|
|
|
@ -301,7 +301,7 @@ module TestHelpers
|
|||
# stderr:: true to pass STDERR output straight to the "real" STDERR.
|
||||
# By default, the STDERR and STDOUT of the process will be
|
||||
# combined in the returned string.
|
||||
def rails(*args, allow_failure: false, stderr: false)
|
||||
def rails(*args, allow_failure: false, stderr: false, stdin: File::NULL)
|
||||
args = args.flatten
|
||||
fork = true
|
||||
|
||||
|
@ -328,7 +328,7 @@ module TestHelpers
|
|||
out_read.close
|
||||
err_read.close if err_read
|
||||
|
||||
$stdin.reopen(File::NULL, "r")
|
||||
$stdin.reopen(stdin, "r")
|
||||
$stdout.reopen(out_write)
|
||||
$stderr.reopen(err_write)
|
||||
|
||||
|
|
Loading…
Reference in a new issue