From 55ba2da40db96f456408e1e4206a775ec3c1b40b Mon Sep 17 00:00:00 2001 From: Sebastian van Hesteren Date: Wed, 31 Jan 2024 11:34:59 +0100 Subject: [PATCH] Add support for rails credentials --- Gemfile.lock | 2 +- lib/eyaml/railtie.rb | 16 ++- lib/eyaml/version.rb | 2 +- spec/eyaml/railtie_spec.rb | 247 +++++++++++++++++++++++++---------- spec/support/rails_helper.rb | 14 +- 5 files changed, 202 insertions(+), 79 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0629383..f934839 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - eyaml (0.3.0) + eyaml (0.4.0) rbnacl (~> 7.1) thor (~> 1.1) diff --git a/lib/eyaml/railtie.rb b/lib/eyaml/railtie.rb index dbc4a2b..e24feba 100644 --- a/lib/eyaml/railtie.rb +++ b/lib/eyaml/railtie.rb @@ -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 @@ -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 @@ -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 diff --git a/lib/eyaml/version.rb b/lib/eyaml/version.rb index 36ad21d..b6927e5 100644 --- a/lib/eyaml/version.rb +++ b/lib/eyaml/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module EYAML - VERSION = "0.3.0" + VERSION = "0.4.0" end diff --git a/spec/eyaml/railtie_spec.rb b/spec/eyaml/railtie_spec.rb index 2b54fa7..17d6070 100644 --- a/spec/eyaml/railtie_spec.rb +++ b/spec/eyaml/railtie_spec.rb @@ -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 diff --git a/spec/support/rails_helper.rb b/spec/support/rails_helper.rb index d141c40..2f3b3a9 100644 --- a/spec/support/rails_helper.rb +++ b/spec/support/rails_helper.rb @@ -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 @@ -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)