diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 89f7b5991f33cae438fe6e8e24b054aab7c7f8c2..1a6aed7ce4aaf03c59e1415ca977c14f23f2715f 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -4,7 +4,6 @@ require "active_support/key_generator" require "active_support/message_verifier" require "rails/engine" -require "rails/secrets" module Rails # An Engine with the responsibility of coordinating the whole boot process. @@ -386,7 +385,18 @@ def config=(configuration) #:nodoc: def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - secrets.merge! Rails::Secrets.parse(config.paths["config/secrets"].existent, env: Rails.env) + yaml = config.paths["config/secrets"].first + + if File.exist?(yaml) + require "erb" + + all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} + shared_secrets = all_secrets["shared"] + env_secrets = all_secrets[Rails.env] + + secrets.merge!(shared_secrets.deep_symbolize_keys) if shared_secrets + secrets.merge!(env_secrets.deep_symbolize_keys) if env_secrets + end # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 4223c38146c9f18928a1275251b0edb985e44490..6102af3ffff2849fa177e44bd8ad778d1bc1439c 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -2,7 +2,6 @@ require "active_support/notifications" require "active_support/dependencies" require "active_support/descendants_tracker" -require "rails/secrets" module Rails class Application @@ -78,11 +77,6 @@ module Bootstrap initializer :bootstrap_hook, group: :all do |app| ActiveSupport.run_load_hooks(:before_initialize, app) end - - initializer :set_secrets_root, group: :all do - Rails::Secrets.root = root - Rails::Secrets.read_encrypted_secrets = config.read_encrypted_secrets - end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index b0592151b7948275d95a58ce3e847448b23769b8..b0d33f87a32fdb1674eb94b158e7a10a86bd19f3 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -13,8 +13,7 @@ class Configuration < ::Rails::Engine::Configuration :railties_order, :relative_url_root, :secret_key_base, :secret_token, :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, - :read_encrypted_secrets + :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading attr_writer :log_level attr_reader :encoding, :api_only @@ -52,7 +51,6 @@ def initialize(*) @debug_exception_response_format = nil @x = Custom.new @enable_dependency_loading = false - @read_encrypted_secrets = false end def encoding=(value) @@ -82,7 +80,7 @@ def paths @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" - paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}" + paths.add "config/secrets", with: "config/secrets.yml" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 73f9684ca4fafa38996e06c0ecb4c55ef7838845..13f3b90b6d3171e22366d15e695babb58e952b38 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -27,22 +27,15 @@ def environment # :nodoc: end # Receives a namespace, arguments and the behavior to invoke the command. - def invoke(full_namespace, args = [], **config) - namespace = full_namespace = full_namespace.to_s + def invoke(namespace, args = [], **config) + namespace = namespace.to_s + namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) + namespace = "version" if %w( -v --version ).include? namespace - if char = namespace =~ /:(\w+)$/ - command_name, namespace = $1, namespace.slice(0, char) + if command = find_by_namespace(namespace) + command.perform(namespace, args, config) else - command_name = namespace - end - - command_name = "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) - namespace = "version" if %w( -v --version ).include?(command_name) - - if command = find_by_namespace(namespace, command_name) - command.perform(command_name, args, config) - else - find_by_namespace("rake").perform(full_namespace, args, config) + find_by_namespace("rake").perform(namespace, args, config) end end @@ -59,10 +52,8 @@ def invoke(full_namespace, args = [], **config) # # Notice that "rails:commands:webrat" could be loaded as well, what # Rails looks for is the first and last parts of the namespace. - def find_by_namespace(namespace, command_name = nil) # :nodoc: - lookups = [ namespace ] - lookups << "#{namespace}:#{command_name}" if command_name - lookups.concat lookups.map { |lookup| "rails:#{lookup}" } + def find_by_namespace(name) # :nodoc: + lookups = [ name, "rails:#{name}" ] lookup(lookups) diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb index db20c718610e494210404dcdfe09d4e2fe9f7986..14357925360a31d17cbbb903be5648e5f69bdf85 100644 --- a/railties/lib/rails/command/base.rb +++ b/railties/lib/rails/command/base.rb @@ -56,9 +56,7 @@ def inherited(base) #:nodoc: end def perform(command, args, config) # :nodoc: - if Rails::Command::HELP_MAPPINGS.include?(args.first) - command, args = "help", [] - end + command = nil if Rails::Command::HELP_MAPPINGS.include?(args.first) dispatch(command, args.dup, nil, config) end @@ -113,7 +111,7 @@ def usage_path # For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb` # would return `rails/test`. def default_command_root - path = File.expand_path(File.join("../commands", command_root_namespace), __dir__) + path = File.expand_path(File.join("../commands", command_name), __dir__) path if File.exist?(path) end @@ -131,10 +129,6 @@ def create_command(meth) super end end - - def command_root_namespace - (namespace.split(":") - %w( rails )).first - end end def help diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE deleted file mode 100644 index 4b7deb4e2a3bba1d7c6cb8201eb420eabfeab5f5..0000000000000000000000000000000000000000 --- a/railties/lib/rails/commands/secrets/USAGE +++ /dev/null @@ -1,52 +0,0 @@ -=== Storing Encrypted Secrets in Source Control - -The Rails `secrets` commands helps encrypting secrets to slim a production -environment's `ENV` hash. It's also useful for atomic deploys: no need to -coordinate key changes to get everything working as the keys are shipped -with the code. - -=== Setup - -Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key` -and `config/secrets.yml.enc` files. - -The latter contains all the keys to be encrypted while the former holds the -encryption key. - -Don't lose the key! Put it in a password manager your team can access. -Should you lose it no one, including you, will be able to access any encrypted -secrets. -Don't commit the key! Add `config/secrets.yml.key` to your source control's -ignore file. If you use Git, Rails handles this for you. - -Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to -manage. - -You could prepend that to your server's start command like this: - - RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start - - -The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`: - - production: - secret_key_base: so-secret-very-hidden-wow - payment_processing_gateway_key: much-safe-very-gaedwey-wow - -But that's where the similarities between `secrets.yml` and `secrets.yml.enc` -end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and -be encrypted. - -A `shared:` top level key is also supported such that any keys there is merged -into the other environments. - -=== Editing Secrets - -After `bin/rails secrets:setup`, run `bin/rails secrets:edit`. - -That command opens a temporary file in `$EDITOR` with the decrypted contents of -`config/secrets.yml.enc` to edit the encrypted secrets. - -When the temporary file is next saved the contents are encrypted and written to -`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets -from leaking. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb deleted file mode 100644 index 05e0c228e89a710c4bdcb8849fdea7abef5e749f..0000000000000000000000000000000000000000 --- a/railties/lib/rails/commands/secrets/secrets_command.rb +++ /dev/null @@ -1,50 +0,0 @@ -require "active_support" -require "rails/secrets" - -module Rails - module Command - class SecretsCommand < Rails::Command::Base # :nodoc: - def help - say "Usage:\n #{self.class.banner}" - say "" - say self.class.desc - end - - def setup - require "rails/generators" - require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" - - Rails::Generators::EncryptedSecretsGenerator.start - end - - def edit - require_application_and_environment! - - Rails::Secrets.read_for_editing do |tmp_path| - watch tmp_path do - puts "Waiting for secrets file to be saved. Abort with Ctrl-C." - system("\$EDITOR #{tmp_path}") - end - end - - puts "New secrets encrypted and saved." - rescue Interrupt - puts "Aborted changing encrypted secrets: nothing saved." - rescue Rails::Secrets::MissingKeyError => error - say error.message - end - - private - def watch(tmp_path) - mtime, start_time = File.mtime(tmp_path), Time.now - - yield - - editor_exits_after_open = $?.success? && (Time.now - start_time) < 1 - if editor_exits_after_open - sleep 0.250 until File.mtime(tmp_path) != mtime - end - end - end - end -end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 3f1bf6a5bbfb430b2452d2fecf967e2b452f741f..85f66cc416a8132c11f62ae772914f64e669deed 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -214,7 +214,6 @@ def sorted_groups rails.map! { |n| n.sub(/^rails:/, "") } rails.delete("app") rails.delete("plugin") - rails.delete("encrypted_secrets") hidden_namespaces.each { |n| groups.delete(n.to_s) } diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 9c4a77fd1d74f88dc4dfe897f31f7f71000e2388..4a39e43e570113ac2fad37b641601c55f68c52a1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -14,11 +14,6 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Attempt to read encrypted secrets from `config/secrets.yml.enc`. - # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or - # `config/secrets.yml.key`. - config.read_encrypted_secrets = true - # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index 816efcc5b1f4c110e787bf55ba6e48afd4b15692..8e995a5df146c20700e6819c26f6707246b0131a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -23,10 +23,8 @@ development: test: secret_key_base: <%= app_secret %> -# Do not keep production secrets in the unencrypted secrets file. -# Instead, either read values from the environment. -# Or, use `bin/rails secrets:setup` to configure encrypted secrets -# and move the `production:` environment over there. +# Do not keep production secrets in the repository, +# instead read values from the environment. production: secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb deleted file mode 100644 index 8b2921361079767dcc4d8dc07035c42823db4733..0000000000000000000000000000000000000000 --- a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb +++ /dev/null @@ -1,66 +0,0 @@ -require "rails/generators/base" -require "rails/secrets" - -module Rails - module Generators - class EncryptedSecretsGenerator < Base - def add_secrets_key_file - unless File.exist?("config/secrets.yml.key") || File.exist?("config/secrets.yml.enc") - key = Rails::Secrets.generate_key - - say "Adding config/secrets.yml.key to store the encryption key: #{key}" - say "" - say "Save this in a password manager your team can access." - say "" - say "If you lose the key, no one, including you, can access any encrypted secrets." - - say "" - create_file "config/secrets.yml.key", key - say "" - end - end - - def ignore_key_file - if File.exist?(".gitignore") - unless File.read(".gitignore").include?(key_ignore) - say "Ignoring config/secrets.yml.key so it won't end up in Git history:" - say "" - append_to_file ".gitignore", key_ignore - say "" - end - else - say "IMPORTANT: Don't commit config/secrets.yml.key. Add this to your ignore file:" - say key_ignore, :on_green - say "" - end - end - - def add_encrypted_secrets_file - unless File.exist?("config/secrets.yml.enc") - say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted." - say "" - - template "config/secrets.yml.enc" do |prefill| - say "" - say "For now the file contains this but it's been encrypted with the generated key:" - say "" - say prefill, :on_green - say "" - - Secrets.encrypt(prefill) - end - - say "You can edit encrypted secrets with `bin/rails secrets:edit`." - - say "Add this to your config/environments/production.rb:" - say "config.read_encrypted_secrets = true" - end - end - - private - def key_ignore - [ "", "# Ignore encrypted secrets key file.", "config/secrets.yml.key", "" ].join("\n") - end - end - end -end diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc deleted file mode 100644 index 70426a66a53d0ed8dc1f3c267fd90cf6c31ff98a..0000000000000000000000000000000000000000 --- a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc +++ /dev/null @@ -1,3 +0,0 @@ -# See `secrets.yml` for tips on generating suitable keys. -# production: -# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb deleted file mode 100644 index a083914109407f96374dc2e60b381313c1876edd..0000000000000000000000000000000000000000 --- a/railties/lib/rails/secrets.rb +++ /dev/null @@ -1,111 +0,0 @@ -require "yaml" - -module Rails - # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 - class Secrets # :nodoc: - class MissingKeyError < RuntimeError - def initialize - super(<<-end_of_message.squish) - Missing encryption key to decrypt secrets with. - Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"] - end_of_message - end - end - - @read_encrypted_secrets = false - @root = File # Wonky, but ensures `join` uses the current directory. - - class << self - attr_writer :root - attr_accessor :read_encrypted_secrets - - def parse(paths, env:) - paths.each_with_object(Hash.new) do |path, all_secrets| - require "erb" - - secrets = YAML.load(ERB.new(preprocess(path)).result) || {} - all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"] - all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env] - end - end - - def generate_key - cipher = new_cipher - SecureRandom.hex(cipher.key_len)[0, cipher.key_len] - end - - def key - ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key - end - - def encrypt(text) - cipher(:encrypt, text) - end - - def decrypt(data) - cipher(:decrypt, data) - end - - def read - decrypt(IO.binread(path)) - end - - def write(contents) - IO.binwrite("#{path}.tmp", encrypt(contents)) - FileUtils.mv("#{path}.tmp", path) - end - - def read_for_editing - tmp_path = File.join(Dir.tmpdir, File.basename(path)) - IO.binwrite(tmp_path, read) - - yield tmp_path - - write(IO.binread(tmp_path)) - ensure - FileUtils.rm(tmp_path) if File.exist?(tmp_path) - end - - private - def handle_missing_key - raise MissingKeyError - end - - def read_key_file - if File.exist?(key_path) - IO.binread(key_path).strip - end - end - - def key_path - @root.join("config", "secrets.yml.key") - end - - def path - @root.join("config", "secrets.yml.enc").to_s - end - - def preprocess(path) - if path.end_with?(".enc") - if @read_encrypted_secrets - decrypt(IO.binread(path)) - else - "" - end - else - IO.read(path) - end - end - - def new_cipher - OpenSSL::Cipher.new("aes-256-cbc") - end - - def cipher(mode, data) - cipher = new_cipher.public_send(mode) - cipher.key = key - cipher.update(data) << cipher.final - end - end - end -end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 986afb6d2a11a1c4c17ba9fb1250a797287e2fc9..1ac2b4cde0ab9605bbe142aa1037d764ad7c77c3 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -335,7 +335,6 @@ def test_generator_without_skips end assert_file "config/environments/production.rb" do |content| assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) - assert_match(/^ config\.read_encrypted_secrets = true/, content) end end diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb deleted file mode 100644 index 747abf19ed4f1ce51c3aa964c0cb943385539f18..0000000000000000000000000000000000000000 --- a/railties/test/generators/encrypted_secrets_generator_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -require "generators/generators_test_helper" -require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" - -class EncryptedSecretsGeneratorTest < Rails::Generators::TestCase - include GeneratorsTestHelper - - def setup - super - cd destination_root - end - - def test_generates_key_file_and_encrypted_secrets_file - run_generator - - assert_file "config/secrets.yml.key", /[\w\d]+/ - - assert File.exist?("config/secrets.yml.enc") - assert_no_match(/production:\n# external_api_key: [\w\d]+/, IO.binread("config/secrets.yml.enc")) - assert_match(/production:\n# external_api_key: [\w\d]+/, Rails::Secrets.read) - end - - def test_appends_to_gitignore - FileUtils.touch(".gitignore") - - run_generator - - assert_file ".gitignore", /config\/secrets.yml.key/, /(?!config\/secrets.yml.enc)/ - end - - def test_warns_when_ignore_is_missing - assert_match(/Add this to your ignore file/i, run_generator) - end - - def test_doesnt_generate_a_new_key_file_if_already_opted_in_to_encrypted_secrets - FileUtils.mkdir("config") - File.open("config/secrets.yml.enc", "w") { |f| f.puts "already secrety" } - - run_generator - - assert_no_file "config/secrets.yml.key" - end -end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 924503a522f53944bb105cf2ceb0e249dfe96eba..1902eac8621b355210a35e122c4abc1d40a89179 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -22,7 +22,6 @@ require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" require "tmpdir" -require "rails/secrets" module TestHelpers module Paths diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb deleted file mode 100644 index 36e42cf1f9874d1b60f80c44417b206b06c51404..0000000000000000000000000000000000000000 --- a/railties/test/secrets_test.rb +++ /dev/null @@ -1,108 +0,0 @@ -require "abstract_unit" -require "isolation/abstract_unit" -require "rails/generators" -require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" -require "rails/secrets" - -class Rails::SecretsTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - - @old_read_encrypted_secrets, Rails::Secrets.read_encrypted_secrets = - Rails::Secrets.read_encrypted_secrets, true - end - - def teardown - Rails::Secrets.read_encrypted_secrets = @old_read_encrypted_secrets - - teardown_app - end - - test "setting read to false skips parsing" do - Rails::Secrets.read_encrypted_secrets = false - - Dir.chdir(app_path) do - assert_equal Hash.new, Rails::Secrets.parse(%w( config/secrets.yml.enc ), env: "production") - end - end - - test "raises when reading secrets without a key" do - run_secrets_generator do - FileUtils.rm("config/secrets.yml.key") - - assert_raises Rails::Secrets::MissingKeyError do - Rails::Secrets.key - end - end - end - - test "reading with ENV variable" do - run_secrets_generator do - begin - old_key = ENV["RAILS_MASTER_KEY"] - ENV["RAILS_MASTER_KEY"] = IO.binread("config/secrets.yml.key").strip - FileUtils.rm("config/secrets.yml.key") - - assert_match "production:\n# external_api_key", Rails::Secrets.read - ensure - ENV["RAILS_MASTER_KEY"] = old_key - end - end - end - - test "reading from key file" do - run_secrets_generator do - File.binwrite("config/secrets.yml.key", "How do I know you feel it?") - - assert_equal "How do I know you feel it?", Rails::Secrets.key - end - end - - test "editing" do - run_secrets_generator do - decrypted_path = nil - - Rails::Secrets.read_for_editing do |tmp_path| - decrypted_path = tmp_path - - assert_match(/production:\n# external_api_key/, File.read(tmp_path)) - - File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") - end - - assert_not File.exist?(decrypted_path) - assert_equal "Empty streets, empty nights. The Downtown Lights.", Rails::Secrets.read - end - end - - test "merging secrets with encrypted precedence" do - run_secrets_generator do - File.write("config/secrets.yml", <<-end_of_secrets) - test: - yeah_yeah: lets-go-walking-down-this-empty-street - end_of_secrets - - Rails::Secrets.write(<<-end_of_secrets) - test: - yeah_yeah: lets-walk-in-the-cool-evening-light - end_of_secrets - - Rails.application.config.root = app_path - Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching 💃🕺 - assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah - end - end - - private - def run_secrets_generator - Dir.chdir(app_path) do - capture(:stdout) do - Rails::Generators::EncryptedSecretsGenerator.start - end - - yield - end - end -end