Skip to content

Commit

Permalink
Add support for rails credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
svanhesteren committed Jan 31, 2024
1 parent 3e2c32e commit d7b2298
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 79 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
eyaml (0.3.0)
eyaml (0.4.0)
rbnacl (~> 7.1)
thor (~> 1.1)

Expand Down
16 changes: 11 additions & 5 deletions lib/eyaml/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ class Railtie < Rails::Railtie
PRIVATE_KEY_ENV_VAR = "EJSON_PRIVATE_KEY"

config.before_configuration do
secrets_files.each do |file|
secrets_or_credentials = if Rails.version >= "7.2" || Dir.glob(Rails.root.join("config", "credentials.*")).any?
:credentials
else
:secrets
end

secrets_files(secrets_or_credentials).each do |file|
next unless valid?(file)

# If private_key is nil (i.e. when $EJSON_PRIVATE_KEY is not set), EYAML will search
Expand All @@ -19,7 +25,7 @@ class Railtie < Rails::Railtie
.deep_symbolize_keys
.except(:_public_key)

break Rails.application.secrets.deep_merge!(secrets)
break Rails.application.send(secrets_or_credentials).deep_merge!(secrets)
end
end

Expand All @@ -30,11 +36,11 @@ def valid?(pathname)
pathname.exist?
end

def secrets_files
def secrets_files(secrets_or_credentials)
EYAML::SUPPORTED_EXTENSIONS.map do |ext|
[
Rails.root.join("config", "secrets.#{ext}"),
Rails.root.join("config", "secrets.#{Rails.env}.#{ext}")
Rails.root.join("config", "#{secrets_or_credentials}.#{ext}"),
Rails.root.join("config", "#{secrets_or_credentials}.#{Rails.env}.#{ext}")
]
end.flatten
end
Expand Down
2 changes: 1 addition & 1 deletion lib/eyaml/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module EYAML
VERSION = "0.3.0"
VERSION = "0.4.0"
end
247 changes: 180 additions & 67 deletions spec/eyaml/railtie_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,115 +8,228 @@

subject { described_class.instance }

let(:secrets) { secrets_class.new }
it "should be a Railtie" do
is_expected.to(be_a(::Rails::Railtie))
end

before(:each) do
FakeFS::FileSystem.clone(fixtures_root)
context "with credentials" do
let(:credentials) { credentials_class.new }

supported_extensions.each do |ext|
FakeFS::FileUtils.copy_file(
fixtures_root.join("data.#{ext}"),
config_root.join("secrets.env.#{ext}")
)
before(:each) do
FakeFS::FileSystem.clone(fixtures_root)

FakeFS::FileUtils.copy_file(
fixtures_root.join("data.#{ext}"),
config_root.join("secrets.#{ext}")
)
supported_extensions.each do |ext|
FakeFS::FileUtils.copy_file(
fixtures_root.join("data.#{ext}"),
config_root.join("credentials.env.#{ext}")
)

FakeFS::FileUtils.copy_file(
fixtures_root.join("data.#{ext}"),
config_root.join("credentials.#{ext}")
)
end
end
end

it "should be a Railtie" do
is_expected.to(be_a(::Rails::Railtie))
end
context "before configuration" do
before do
allow_rails.to(receive(:root).and_return(fixtures_root))
allow_rails.to(receive_message_chain("application.credentials").and_return(credentials))
end

context "before configuration" do
before do
allow_rails.to(receive(:root).and_return(fixtures_root))
allow_rails.to(receive_message_chain("application.secrets").and_return(secrets))
end
it "merges credentials into application credentials" do
run_load_hooks
expect(credentials).to(include(:secret))
end

it "merges secrets into application secrets" do
run_load_hooks
expect(secrets).to(include(:secret))
end
it "decrypts data before merging" do
run_load_hooks
expect(credentials).to(include(secret: "password"))
end

it "uses $EJSON_PRIVATE_KEY instead of checking locally if it's set" do
File.delete(public_key_path)
allow(ENV).to receive(:[]).with("EJSON_PRIVATE_KEY").and_return(private_key)

run_load_hooks
expect(credentials).to(include(secret: "password"))
end

describe "prioritizes 'credentials' with extension" do
it "eyaml" do
remove_all_that_dont_end_with(".eyaml")
run_load_hooks
expect(credentials).to(include(_extension: "eyaml"))
end

it "eyml" do
remove_all_that_dont_end_with(".eyml")
run_load_hooks
expect(credentials).to(include(_extension: "eyml"))
end

it "ejson" do
remove_all_that_dont_end_with(".ejson")
run_load_hooks
expect(credentials).to(include(_extension: "ejson"))
end
end

context "without credentials" do
before { remove_files(type: :credentials) }

describe "falls back to 'credentials.env' with extension" do
before { allow_rails.to(receive(:env).and_return(:env)) }

it "eyaml" do
remove_all_that_dont_end_with(".eyaml")
run_load_hooks
expect(credentials).to(include(_extension: "eyaml"))
end

it "eyml" do
remove_all_that_dont_end_with(".eyml")

run_load_hooks
expect(credentials).to(include(_extension: "eyml"))
end

it "decrypts data before merging" do
run_load_hooks
expect(secrets).to(include(secret: "password"))
it "ejson" do
remove_all_that_dont_end_with(".ejson")
run_load_hooks
expect(credentials).to(include(_extension: "ejson"))
end
end

it "does not load anything when Rails.env doesn't match" do
expect(Rails).to(receive(:env).and_return(:production).at_least(:once))
run_load_hooks
expect(credentials).to(be_empty)
end
end

context "without any eyaml" do
before do
remove_files(type: :credentials)
remove_environment_files(type: :credentials)
end

it "does not load anything" do
expect(Rails).to(receive(:env).and_return(:production).at_least(:once))
run_load_hooks
expect(credentials).to(be_empty)
end
end
end
end

context "with secrets" do
let(:secrets) { secrets_class.new }

it "uses $EJSON_PRIVATE_KEY instead of checking locally if it's set" do
File.delete(public_key_path)
allow(ENV).to receive(:[]).with("EJSON_PRIVATE_KEY").and_return(private_key)
before(:each) do
FakeFS::FileSystem.clone(fixtures_root)

run_load_hooks
expect(secrets).to(include(secret: "password"))
supported_extensions.each do |ext|
FakeFS::FileUtils.copy_file(
fixtures_root.join("data.#{ext}"),
config_root.join("secrets.env.#{ext}")
)

FakeFS::FileUtils.copy_file(
fixtures_root.join("data.#{ext}"),
config_root.join("secrets.#{ext}")
)
end
end

describe "prioritizes 'secrets' with extension" do
it "eyaml" do
remove_all_secrets_that_dont_end_with(".eyaml")
run_load_hooks
expect(secrets).to(include(_extension: "eyaml"))
context "before configuration" do
before do
allow_rails.to(receive(:root).and_return(fixtures_root))
allow_rails.to(receive_message_chain("application.secrets").and_return(secrets))
end

it "eyml" do
remove_all_secrets_that_dont_end_with(".eyml")
it "merges secrets into application secrets" do
run_load_hooks
expect(secrets).to(include(_extension: "eyml"))
expect(secrets).to(include(:secret))
end

it "ejson" do
remove_all_secrets_that_dont_end_with(".ejson")
it "decrypts data before merging" do
run_load_hooks
expect(secrets).to(include(_extension: "ejson"))
expect(secrets).to(include(secret: "password"))
end
end

context "without secrets" do
before { remove_secrets_files }
it "uses $EJSON_PRIVATE_KEY instead of checking locally if it's set" do
File.delete(public_key_path)
allow(ENV).to receive(:[]).with("EJSON_PRIVATE_KEY").and_return(private_key)

describe "falls back to 'secrets.env' with extension" do
before { allow_rails.to(receive(:env).and_return(:env)) }
run_load_hooks
expect(secrets).to(include(secret: "password"))
end

describe "prioritizes 'secrets' with extension" do
it "eyaml" do
remove_all_secrets_that_dont_end_with(".eyaml")
remove_all_that_dont_end_with(".eyaml")
run_load_hooks
expect(secrets).to(include(_extension: "eyaml"))
end

it "eyml" do
remove_all_secrets_that_dont_end_with(".eyml")

remove_all_that_dont_end_with(".eyml")
run_load_hooks
expect(secrets).to(include(_extension: "eyml"))
end

it "ejson" do
remove_all_secrets_that_dont_end_with(".ejson")
remove_all_that_dont_end_with(".ejson")
run_load_hooks
expect(secrets).to(include(_extension: "ejson"))
end
end

it "does not load anything when Rails.env doesn't match" do
expect(Rails).to(receive(:env).and_return(:production).at_least(:once))
run_load_hooks
expect(secrets).to(be_empty)
end
end
context "without secrets" do
before { remove_files(type: :secrets) }

context "without any eyaml" do
before do
remove_secrets_files
remove_environment_secrets_files
describe "falls back to 'secrets.env' with extension" do
before { allow_rails.to(receive(:env).and_return(:env)) }

it "eyaml" do
remove_all_that_dont_end_with(".eyaml")
run_load_hooks
expect(secrets).to(include(_extension: "eyaml"))
end

it "eyml" do
remove_all_that_dont_end_with(".eyml")

run_load_hooks
expect(secrets).to(include(_extension: "eyml"))
end

it "ejson" do
remove_all_that_dont_end_with(".ejson")
run_load_hooks
expect(secrets).to(include(_extension: "ejson"))
end
end

it "does not load anything when Rails.env doesn't match" do
expect(Rails).to(receive(:env).and_return(:production).at_least(:once))
run_load_hooks
expect(secrets).to(be_empty)
end
end

it "does not load anything" do
expect(Rails).to(receive(:env).and_return(:production).at_least(:once))
run_load_hooks
expect(secrets).to(be_empty)
context "without any eyaml" do
before do
remove_files(type: :secrets)
remove_environment_files(type: :secrets)
end

it "does not load anything" do
expect(Rails).to(receive(:env).and_return(:production).at_least(:once))
run_load_hooks
expect(secrets).to(be_empty)
end
end
end
end
Expand Down
14 changes: 9 additions & 5 deletions spec/support/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def secrets_class
ActiveSupport::OrderedOptions
end

def credentials_class
ActiveSupport::OrderedOptions
end

def run_load_hooks
ActiveSupport.run_load_hooks(:before_configuration)
end
Expand All @@ -24,19 +28,19 @@ def config_root
fixtures_root.join("config")
end

def remove_secrets_files
def remove_files(type: :secrets)
supported_extensions.each do |ext|
File.delete(config_root.join("secrets.#{ext}"))
File.delete(config_root.join("#{type}.#{ext}"))
end
end

def remove_environment_secrets_files
def remove_environment_files(type: :secrets)
supported_extensions.each do |ext|
File.delete(config_root.join("secrets.env.#{ext}"))
File.delete(config_root.join("#{type}.env.#{ext}"))
end
end

def remove_all_secrets_that_dont_end_with(ext)
def remove_all_that_dont_end_with(ext)
Dir[config_root.join("*")].each do |secret_path|
next if secret_path.end_with?(ext)
File.delete(secret_path)
Expand Down

0 comments on commit d7b2298

Please sign in to comment.