Skip to content

Commit 9a64857

Browse files
authored
Merge pull request rails#53119 from n-studio/fetch_credentials
Add credentials:fetch command
2 parents 7409209 + ccf22d6 commit 9a64857

File tree

5 files changed

+57
-7
lines changed

5 files changed

+57
-7
lines changed

railties/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Add command `rails credentials:fetch PATH` to get the value of a credential from the credentials file.
2+
3+
```bash
4+
$ bin/rails credentials:fetch kamal_registry/password
5+
```
6+
7+
*Matthew Nguyen*, *Jean Boussier*
8+
19
* Generate static BCrypt password digests in fixtures instead of dynamic ERB expressions.
210

311
Previously, fixtures with password digest attributes used `<%= BCrypt::Password.create("secret") %>`,

railties/lib/rails/commands/credentials/credentials_command.rb

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def edit
3535
def show
3636
load_environment_config!
3737

38-
say credentials.read.presence || missing_credentials_message
38+
say credentials.read.presence || missing_credentials!
3939
end
4040

4141
desc "diff", "Enroll/disenroll in decrypted diffs of credentials using git"
@@ -57,6 +57,26 @@ def diff(content_path = nil)
5757
say credentials.content_path.read
5858
end
5959

60+
desc "fetch PATH", "Fetch a value in the decrypted credentials"
61+
def fetch(path)
62+
load_environment_config!
63+
64+
if (yaml = credentials.read)
65+
begin
66+
value = YAML.load(yaml)
67+
value = path.split(".").inject(value) do |doc, key|
68+
doc.fetch(key)
69+
end
70+
say value.to_s
71+
rescue KeyError, NoMethodError
72+
say_error "Invalid or missing credential path: #{path}"
73+
exit 1
74+
end
75+
else
76+
missing_credentials!
77+
end
78+
end
79+
6080
private
6181
def config
6282
Rails.application.config.credentials
@@ -114,12 +134,13 @@ def warn_if_credentials_are_invalid
114134
say "Your application will not be able to load '#{content_path}' until the error has been fixed.", :red
115135
end
116136

117-
def missing_credentials_message
137+
def missing_credentials!
118138
if !credentials.key?
119-
"Missing '#{key_path}' to decrypt credentials. See `#{executable(:help)}`."
139+
say_error "Missing '#{key_path}' to decrypt credentials. See `#{executable(:help)}`."
120140
else
121-
"File '#{content_path}' does not exist. Use `#{executable(:edit)}` to change that."
141+
say_error "File '#{content_path}' does not exist. Use `#{executable(:edit)}` to change that."
122142
end
143+
exit 1
123144
end
124145

125146
def relative_path(path)

railties/lib/rails/generators/rails/app/templates/kamal-secrets.tt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})
88
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
99

10+
# Example of extracting secrets from Rails credentials
11+
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
12+
1013
# Use a GITHUB_TOKEN if private repositories are needed for the image
1114
# GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
1215

railties/test/command/base_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Rails::Command::BaseTest < ActiveSupport::TestCase
1414
end
1515

1616
test "printing commands returns namespaced commands" do
17-
assert_equal %w(credentials:edit credentials:show credentials:diff), Rails::Command::CredentialsCommand.printing_commands.map(&:first)
17+
assert_equal %w(credentials:edit credentials:show credentials:diff credentials:fetch), Rails::Command::CredentialsCommand.printing_commands.map(&:first)
1818
assert_equal %w(db:system:change), Rails::Command::Db::System::ChangeCommand.printing_commands.map(&:first)
1919
end
2020

railties/test/commands/credentials_test.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ class Rails::Command::CredentialsTest < ActiveSupport::TestCase
170170
assert_match %r/foo: bar: bad/, run_edit_command
171171
end
172172

173-
174173
test "show credentials" do
175174
assert_match DEFAULT_CREDENTIALS_PATTERN, run_show_command
176175
end
@@ -186,7 +185,8 @@ class Rails::Command::CredentialsTest < ActiveSupport::TestCase
186185
remove_file "config/master.key"
187186
add_to_config "config.require_master_key = false"
188187

189-
assert_match(/Missing 'config\/master\.key' to decrypt credentials/, run_show_command)
188+
stderr_output = capture(:stderr) { run_show_command(stderr: true, allow_failure: true) }
189+
assert_match(/Missing 'config\/master\.key' to decrypt credentials/, stderr_output)
190190
end
191191

192192
test "show command displays content specified by environment option" do
@@ -350,6 +350,20 @@ class Rails::Command::CredentialsTest < ActiveSupport::TestCase
350350
assert_credentials_paths "config/credentials/production.yml.enc", key_path, environment: "production"
351351
end
352352

353+
test "fetch value for given path" do
354+
write_credentials({ "foo" => { "bar" => { "baz" => 42 } } }.to_yaml)
355+
356+
assert_match(/42/, run_fetch_command("foo.bar.baz"))
357+
end
358+
359+
test "fetch missing key" do
360+
write_credentials({ "foo" => { "bar" => { "baz" => 42 } } }.to_yaml)
361+
362+
363+
stderr_output = capture(:stderr) { run_fetch_command("egg.spam", stderr: true, allow_failure: true) }
364+
assert_match("Invalid or missing credential path: egg.spam", stderr_output)
365+
end
366+
353367
private
354368
DEFAULT_CREDENTIALS_PATTERN = /access_key_id: 123\n.*secret_key_base: \h{128}\n/m
355369

@@ -372,6 +386,10 @@ def run_diff_command(path = nil, enroll: nil, disenroll: nil, **options)
372386
rails "credentials:diff", args, **options
373387
end
374388

389+
def run_fetch_command(path, **options)
390+
rails "credentials:fetch", path, **options
391+
end
392+
375393
def write_credentials(content, **options)
376394
switch_env("CONTENT", content) do
377395
run_edit_command(visual: %(ruby -e "File.write ARGV[0], ENV['CONTENT']"), **options)

0 commit comments

Comments
 (0)