diff options
74 files changed, 538 insertions, 596 deletions
@@ -13,7 +13,7 @@ gem "rake", ">= 11.1" # This needs to be with require false to ensure correct loading order, as it has to # be loaded after loading the test library. -gem "mocha", "~> 0.14", require: false +gem "mocha", require: false gem "capybara", "~> 2.15" diff --git a/Gemfile.lock b/Gemfile.lock index fb18fdc14e..938b4a71cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -309,7 +309,7 @@ GEM path_expander (~> 1.0) minitest-server (1.0.4) minitest (~> 5.0) - mocha (0.14.0) + mocha (1.3.0) metaclass (~> 0.0.1) mono_logger (1.1.0) msgpack (1.1.0) @@ -506,7 +506,7 @@ DEPENDENCIES listen (>= 3.0.5, < 3.2) mini_magick minitest-bisect - mocha (~> 0.14) + mocha mysql2 (>= 0.4.4) nokogiri (>= 1.6.8) pg (>= 0.18.0) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 1d4b27a0f9..16090e7946 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -7,14 +7,14 @@ *Michael J Coyne* -* Use Capybara registered `:puma` server config. +* Use Capybara registered `:puma` server config. The Capybara registered `:puma` server ensures the puma server is run in process so connection sharing and open request detection work correctly by default. *Thomas Walpole* -* Cookies `:expires` option supports `ActiveSupport::Duration` object. +* Cookies `:expires` option supports `ActiveSupport::Duration` object. cookies[:user_name] = { value: "assain", expires: 1.hour } cookies[:key] = { value: "a yummy cookie", expires: 6.months } @@ -23,7 +23,7 @@ *Assain Jaleel* -* Enforce signed/encrypted cookie expiry server side. +* Enforce signed/encrypted cookie expiry server side. Rails can thwart attacks by malicious clients that don't honor a cookie's expiry. diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index b3831649a8..0213987c99 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -264,9 +264,9 @@ module ActionDispatch end def upgrade_legacy_hmac_aes_cbc_cookies? - request.secret_key_base.present? && - request.encrypted_signed_cookie_salt.present? && - request.encrypted_cookie_salt.present? && + request.secret_key_base.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? && request.use_authenticated_cookie_encryption end @@ -570,12 +570,12 @@ module ActionDispatch secret = request.key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER) - request.cookies_rotations.signed.each do |rotation_options| - @verifier.rotate serializer: SERIALIZER, **rotation_options + request.cookies_rotations.signed.each do |*secrets, **options| + @verifier.rotate(*secrets, serializer: SERIALIZER, **options) end if upgrade_legacy_signed_cookies? - @verifier.rotate raw_key: request.secret_token, serializer: SERIALIZER + @verifier.rotate request.secret_token, serializer: SERIALIZER end end @@ -603,14 +603,16 @@ module ActionDispatch secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len) @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER) - request.cookies_rotations.encrypted.each do |rotation_options| - @encryptor.rotate serializer: SERIALIZER, **rotation_options + request.cookies_rotations.encrypted.each do |*secrets, **options| + @encryptor.rotate(*secrets, serializer: SERIALIZER, **options) end if upgrade_legacy_hmac_aes_cbc_cookies? - @encryptor.rotate \ - key_generator: request.key_generator, salt: request.encrypted_cookie_salt, signed_salt: request.encrypted_signed_cookie_salt, - cipher: "aes-256-cbc", digest: digest, serializer: SERIALIZER + legacy_cipher = "aes-256-cbc" + secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher)) + sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt) + + @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER) end if upgrade_legacy_signed_cookies? diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 706d0be9c2..70587fa2b0 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -461,37 +461,13 @@ class CookiesTest < ActionController::TestCase assert_equal verifier.generate(45), cookies[:user_id] end - def test_signed_cookie_rotations_with_secret_key_base_and_digest - rotated_secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" - rotated_salt = "signed cookie" + def test_signed_cookie_rotating_secret_and_digest + secret = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" - @request.env["action_dispatch.cookies_rotations"].rotate :signed, - secret: rotated_secret_key_base, salt: rotated_salt, digest: "SHA1" - - old_secret = ActiveSupport::KeyGenerator.new(rotated_secret_key_base, iterations: 1000).generate_key(rotated_salt) - old_message = ActiveSupport::MessageVerifier.new(old_secret, digest: "SHA1", serializer: Marshal).generate(45) - - @request.headers["Cookie"] = "user_id=#{old_message}" - - get :get_signed_cookie - assert_equal 45, @controller.send(:cookies).signed[:user_id] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) - verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal) - assert_equal 45, verifier.verify(@response.cookies["user_id"]) - end - - def test_signed_cookie_rotations_with_raw_key_and_digest - rotated_raw_key = "b3c631c314c0bbca50c1b2843150fe33" - - @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" - @request.env["action_dispatch.cookies_rotations"].rotate :signed, - raw_key: rotated_raw_key, digest: "SHA1" - - old_message = ActiveSupport::MessageVerifier.new(rotated_raw_key, digest: "SHA1", serializer: Marshal).generate(45) + @request.env["action_dispatch.cookies_rotations"].rotate :signed, secret, digest: "SHA1" + old_message = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal).generate(45) @request.headers["Cookie"] = "user_id=#{old_message}" get :get_signed_cookie @@ -993,40 +969,15 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end - def test_encrypted_cookie_rotations_with_secret_and_salt - rotated_secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" - rotated_salt = "authenticated encrypted cookie" - - @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm" - @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, - secret: rotated_secret_key_base, salt: rotated_salt, cipher: "aes-256-gcm" - - key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm") - - old_secret = ActiveSupport::KeyGenerator.new(rotated_secret_key_base, iterations: 1000).generate_key(rotated_salt, key_len) - old_message = ActiveSupport::MessageEncryptor.new(old_secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign("bar") - - @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}" - - get :get_encrypted_cookie - assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len) - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) - end - - def test_encrypted_cookie_rotations_with_raw_key - raw_key = "b3c631c314c0bbca50c1b2843150fe33" + def test_encrypted_cookie_rotating_secret + secret = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm" - @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, - raw_key: raw_key, cipher: "aes-256-gcm" + @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm") - old_message = ActiveSupport::MessageEncryptor.new(raw_key, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45) + old_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45) @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}" diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index c98270bd12..f20a66c2d2 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -26,14 +26,6 @@ require "active_record" require "pp" # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late -module Rails - class << self - def env - @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") - end - end -end - ActiveSupport::Dependencies.hook! Thread.abort_on_exception = true @@ -110,12 +102,6 @@ module ActionDispatch end end -module ActiveSupport - class TestCase - include ActionDispatch::DrawOnce - end -end - class RoutedRackApp attr_reader :routes @@ -162,29 +148,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase self.app = build_app - # Stub Rails dispatcher so it does not get controller references and - # simply return the controller#action as Rack::Body. - class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher - private - def controller_reference(controller_param) - controller_param - end - - def dispatch(controller, action, env) - [200, { "Content-Type" => "text/html" }, ["#{controller}##{action}"]] - end - end - - def self.stub_controllers - old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } - yield ActionDispatch::Routing::RouteSet.new - ensure - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } - end - def with_routing(&block) temporary_routes = ActionDispatch::Routing::RouteSet.new old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes) @@ -196,21 +159,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase self.class.app = old_app silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } end - - def with_autoload_path(path) - path = File.join(File.expand_path("fixtures", __dir__), path) - if ActiveSupport::Dependencies.autoload_paths.include?(path) - yield - else - begin - ActiveSupport::Dependencies.autoload_paths << path - yield - ensure - ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path } - ActiveSupport::Dependencies.clear - end - end - end end ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) @@ -274,6 +222,7 @@ module ActionDispatch end class ActiveSupport::TestCase + include ActionDispatch::DrawOnce include ActiveSupport::Testing::MethodCallAssertions # Skips the current run on Rubinius using Minitest::Assertions#skip diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index c7714cf205..0e690c82cb 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -21,8 +21,8 @@ class SanitizeHelperTest < ActionView::TestCase def test_should_sanitize_illegal_style_properties raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;) - expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;) - assert_equal expected, sanitize_css(raw) + expected = %r(\Adisplay:\s?block;\s?width:\s?100%;\s?height:\s?100%;\s?background-color:\s?black;\s?background-x:\s?center;\s?background-y:\s?center;\z) + assert_match expected, sanitize_css(raw) end def test_strip_tags diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c34236d4be..f73e27b91f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,28 @@ +* PostgreSQL `tsrange` now preserves subsecond precision + + PostgreSQL 9.1+ introduced range types, and Rails added support for using + this datatype in ActiveRecord. However, the serialization of + PostgreSQL::OID::Range was incomplete, because it did not properly + cast the bounds that make up the range. This led to subseconds being + dropped in SQL commands: + + (byebug) from = type_cast_single_for_database(range.first) + 2010-01-01 13:30:00 UTC + + (byebug) to = type_cast_single_for_database(range.last) + 2011-02-02 19:30:00 UTC + + (byebug) "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + "[2010-01-01 13:30:00 UTC,2011-02-02 19:30:00 UTC)" + + (byebug) "[#{type_cast(from)},#{type_cast(to)}#{value.exclude_end? ? ')' : ']'}" + "['2010-01-01 13:30:00.670277','2011-02-02 19:30:00.745125')" + +* Passing a `Set` to `Relation#where` now behaves the same as passing an + array. + + *Sean Griffin* + * Use given algorithm while removing index from database. Fixes #24190. diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index fe696e0d6e..607d376a08 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -50,20 +50,14 @@ module ActiveRecord end def owner_keys - unless defined?(@owner_keys) - @owner_keys = owners.map do |owner| - owner[owner_key_name] - end - @owner_keys.uniq! - @owner_keys.compact! - end - @owner_keys + @owner_keys ||= owners_by_key.keys end def owners_by_key unless defined?(@owners_by_key) @owners_by_key = owners.each_with_object({}) do |owner, h| - h[convert_key(owner[owner_key_name])] = owner + key = convert_key(owner[owner_key_name]) + h[key] = owner if key end end @owners_by_key diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 4f957ac3ca..06598439d8 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,7 +34,7 @@ module ActiveRecord def reload(*) super.tap do @mutations_before_last_save = nil - clear_mutation_trackers + @mutations_from_database = nil @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end end @@ -44,22 +44,21 @@ module ActiveRecord @attributes = self.class._default_attributes.map do |attr| attr.with_value_from_user(@attributes.fetch_value(attr.name)) end - clear_mutation_trackers + @mutations_from_database = nil end def changes_applied # :nodoc: - @mutations_before_last_save = mutation_tracker - @mutations_from_database = AttributeMutationTracker.new(@attributes) + @mutations_before_last_save = mutations_from_database @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments - clear_mutation_trackers + @mutations_from_database = nil end def clear_changes_information # :nodoc: @mutations_before_last_save = nil @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments - clear_mutation_trackers + @mutations_from_database = nil end def clear_attribute_changes(attr_names) # :nodoc: @@ -75,7 +74,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(mutation_tracker.changed_values).freeze + super.reverse_merge(mutations_from_database.changed_values).freeze end end @@ -90,7 +89,7 @@ module ActiveRecord end def attribute_changed_in_place?(attr_name) # :nodoc: - mutation_tracker.changed_in_place?(attr_name) + mutations_from_database.changed_in_place?(attr_name) end # Did this attribute change when we last saved? This method can be invoked @@ -183,26 +182,18 @@ module ActiveRecord result end - def mutation_tracker - unless defined?(@mutation_tracker) - @mutation_tracker = nil - end - @mutation_tracker ||= AttributeMutationTracker.new(@attributes) - end - def mutations_from_database unless defined?(@mutations_from_database) @mutations_from_database = nil end - @mutations_from_database ||= mutation_tracker + @mutations_from_database ||= AttributeMutationTracker.new(@attributes) end def changes_include?(attr_name) - super || mutation_tracker.changed?(attr_name) + super || mutations_from_database.changed?(attr_name) end def clear_attribute_change(attr_name) - mutation_tracker.forget_change(attr_name) mutations_from_database.forget_change(attr_name) end @@ -227,11 +218,6 @@ module ActiveRecord @attributes = @attributes.map(&:forgetting_assignment) end - def clear_mutation_trackers - @mutation_tracker = nil - @mutations_from_database = nil - end - def mutations_before_last_save @mutations_before_last_save ||= NullMutationTracker.instance end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 63c059e291..d8fc046e10 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -17,13 +17,15 @@ module ActiveRecord # Returns the primary key value. def id sync_with_transaction_state - _read_attribute(self.class.primary_key) if self.class.primary_key + primary_key = self.class.primary_key + _read_attribute(primary_key) if primary_key end # Sets the primary key value. def id=(value) sync_with_transaction_state - _write_attribute(self.class.primary_key, value) if self.class.primary_key + primary_key = self.class.primary_key + _write_attribute(primary_key, value) if primary_key end # Queries the primary key value. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index b070235684..4077250583 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -58,8 +58,9 @@ module ActiveRecord attr_name.to_s end - name = self.class.primary_key if name == "id".freeze && self.class.primary_key - sync_with_transaction_state if name == self.class.primary_key + primary_key = self.class.primary_key + name = primary_key if name == "id".freeze && primary_key + sync_with_transaction_state if name == primary_key _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 37891ce2ef..bb0ec6a8c3 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -39,8 +39,9 @@ module ActiveRecord attr_name.to_s end - name = self.class.primary_key if name == "id".freeze && self.class.primary_key - sync_with_transaction_state if name == self.class.primary_key + primary_key = self.class.primary_key + name = primary_key if name == "id".freeze && primary_key + sync_with_transaction_state if name == primary_key _write_attribute(name, value) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 1041db0b8f..be2f625d74 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -396,6 +396,9 @@ module ActiveRecord alias :belongs_to :references def new_column_definition(name, type, **options) # :nodoc: + if integer_like_primary_key?(type, options) + type = integer_like_primary_key_type(type, options) + end type = aliased_types(type.to_s, type) options[:primary_key] ||= type == :primary_key options[:null] = false if options[:primary_key] @@ -414,6 +417,10 @@ module ActiveRecord def integer_like_primary_key?(type, options) options[:primary_key] && [:integer, :bigint].include?(type) && !options.key?(:default) end + + def integer_like_primary_key_type(type, options) + type + end end class AlterTable # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index c9607df28c..4f0c1890be 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -2,7 +2,7 @@ require_relative "../../migration/join_table" require "active_support/core_ext/string/access" -require "digest" +require "digest/sha2" module ActiveRecord module ConnectionAdapters # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index eff96af87a..da25e4863c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -57,17 +57,12 @@ module ActiveRecord include ColumnMethods def new_column_definition(name, type, **options) # :nodoc: - if integer_like_primary_key?(type, options) - options[:auto_increment] = true - end - case type when :virtual type = options[:type] when :primary_key type = :integer options[:limit] ||= 8 - options[:auto_increment] = true options[:primary_key] = true when /\Aunsigned_(?<type>.+)\z/ type = $~[:type].to_sym @@ -81,6 +76,11 @@ module ActiveRecord def aliased_types(name, fallback) fallback end + + def integer_like_primary_key_type(type, options) + options[:auto_increment] = true + type + end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 7d5d7d91e6..a89aa5ea09 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -35,7 +35,7 @@ module ActiveRecord if value.is_a?(::Range) from = type_cast_single_for_database(value.begin) to = type_cast_single_for_database(value.end) - "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + ::Range.new(from, to, value.exclude_end?) else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index a0a22ba0f1..9fdeab06c1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -101,6 +101,8 @@ module ActiveRecord end when OID::Array::Data _quote(encode_array(value)) + when Range + _quote(encode_range(value)) else super end @@ -117,6 +119,8 @@ module ActiveRecord value.to_s when OID::Array::Data encode_array(value) + when Range + encode_range(value) else super end @@ -133,6 +137,10 @@ module ActiveRecord result end + def encode_range(range) + "[#{type_cast(range.first)},#{type_cast(range.last)}#{range.exclude_end? ? ')' : ']'}" + end + def determine_encoding_of_strings_in_array(value) case value when ::Array then determine_encoding_of_strings_in_array(value.first) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index cb13f9fec1..75622eb304 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -179,17 +179,14 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - def new_column_definition(name, type, **options) # :nodoc: - if integer_like_primary_key?(type, options) - type = if type == :bigint || options[:limit] == 8 + private + def integer_like_primary_key_type(type, options) + if type == :bigint || options[:limit] == 8 :bigserial else :serial end end - - super - end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb index 2010de1ce2..c9855019c1 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -9,13 +9,10 @@ module ActiveRecord end alias :belongs_to :references - def new_column_definition(name, type, **options) # :nodoc: - if integer_like_primary_key?(type, options) - type = :primary_key + private + def integer_like_primary_key_type(type, options) + :primary_key end - - super - end end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b48a137a73..a57c60ffac 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -322,7 +322,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) + became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 82ab2415e1..97adfb4352 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -425,9 +425,8 @@ module ActiveRecord def initialize(name, scope, options, active_record) super - @automatic_inverse_of = nil @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type") - @foreign_type = options[:foreign_type] || "#{name}_type" + @foreign_type = options[:polymorphic] && (options[:foreign_type] || "#{name}_type") @constructable = calculate_constructable(macro, options) @association_scope_cache = Concurrent::Map.new @@ -609,12 +608,14 @@ module ActiveRecord # If it cannot find a suitable inverse association name, it returns # +nil+. def inverse_name - options.fetch(:inverse_of) do - @automatic_inverse_of ||= automatic_inverse_of + unless defined?(@inverse_name) + @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of } end + + @inverse_name end - # returns either false or the inverse association name that it finds. + # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym @@ -631,8 +632,6 @@ module ActiveRecord return inverse_name end end - - false end # Checks if the inverse reflection that is returned from the diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 5c42414072..be4b169f67 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -13,6 +13,7 @@ module ActiveRecord register_handler(Range, RangeHandler.new(self)) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) + register_handler(Set, ArrayHandler.new(self)) end def build_from_hash(attributes) diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index b4a776d04d..a75fdef698 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -232,6 +232,57 @@ _SQL end end + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end + + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end + + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end + + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end + + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + end + end + def test_create_numrange assert_equal_round_trip(@new_range, :num_range, BigDecimal.new("0.5")...BigDecimal.new("1")) diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index 449023b6eb..8212ed4263 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -30,6 +30,6 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase big_range = 0..123456789123456789 assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) } - assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) + assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range)) end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 0ea8ef5cea..2d67c57cfb 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1019,14 +1019,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.time_zone_aware_types = old_types end - def cached_columns - Topic.columns.map(&:name) - end - - def time_related_columns_on_topic - Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) } - end - def privatize(method_signature) @target.class_eval(<<-private_method, __FILE__, __LINE__ + 1) private diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index ae1dc35bff..4edaf79e9a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1806,6 +1806,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).take end + test "arel_attribute respects a custom table" do + assert_equal [posts(:welcome)], custom_post_relation.ranked_by_comments.limit_by(1).to_a + end + test "#load" do relation = Post.all assert_queries(1) do @@ -1912,6 +1916,19 @@ class RelationTest < ActiveRecord::TestCase end end + test "#where with set" do + david = authors(:david) + mary = authors(:mary) + + authors = Author.where(name: ["David", "Mary"].to_set) + assert_equal [david, mary], authors + end + + test "#where with empty set" do + authors = Author.where(name: Set.new) + assert_empty authors + end + private def custom_post_relation table_alias = Post.arel_table.alias("omg_posts") diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 6302e84884..ca1defa332 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -229,7 +229,6 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) - Kernel.stubs(:system) end def teardown @@ -333,7 +332,6 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) - Kernel.stubs(:system) end def test_structure_load diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 4c8e847354..935a11e811 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -21,7 +21,7 @@ class Post < ActiveRecord::Base scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") } - scope :ranked_by_comments, -> { order("comments_count DESC") } + scope :ranked_by_comments, -> { order(arel_attribute(:comments_count).desc) } scope :limit_by, lambda { |l| limit(l) } scope :locked, -> { lock } diff --git a/activestorage/Rakefile b/activestorage/Rakefile index aa71a65f6e..2aa4d2a76f 100644 --- a/activestorage/Rakefile +++ b/activestorage/Rakefile @@ -11,4 +11,6 @@ Rake::TestTask.new do |test| test.warning = false end +task :package + task default: :test diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 487984cbd3..493ebeb01f 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,7 @@ +* Return an instance of `HashWithIndifferentAccess` from `HashWithIndifferentAccess#transform_keys`. + + *Yuji Yaginuma* + * Add key rotation support to `MessageEncryptor` and `MessageVerifier` This change introduces a `rotate` method to both the `MessageEncryptor` and diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 12291af443..fcc13feb8c 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -306,6 +306,11 @@ module ActiveSupport dup.tap { |hash| hash.transform_values!(*args, &block) } end + def transform_keys(*args, &block) + return to_enum(:transform_keys) unless block_given? + dup.tap { |hash| hash.transform_keys!(*args, &block) } + end + def compact dup.tap(&:compact!) end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 003fb4c354..8a1918039c 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -57,31 +57,27 @@ module ActiveSupport # # === Rotating keys # - # This class also defines a +rotate+ method which can be used to rotate out - # encryption keys no longer in use. - # - # This method is called with an options hash where a +:cipher+ option and - # either a +:raw_key+ or +:secret+ option must be defined. If +:raw_key+ is - # defined, it is used directly for the underlying encryption function. If - # the +:secret+ option is defined, a +:salt+ option must also be defined and - # a +KeyGenerator+ instance will be used to derive a key using +:salt+. When - # +:secret+ is used, a +:key_generator+ option may also be defined allowing - # for custom +KeyGenerator+ instances. If CBC encryption is used a - # `:raw_signed_key` or a `:signed_salt` option must also be defined. A - # +:digest+ may also be defined when using CBC encryption. This method can be - # called multiple times and new encryptor instances will be added to the - # rotation stack on each call. - # - # # Specifying the key used for encryption - # crypt.rotate raw_key: old_aead_key, cipher: "aes-256-gcm" - # crypt.rotate raw_key: old_cbc_key, raw_signed_key: old_cbc_sign_key, cipher: "aes-256-cbc", digest: "SHA1" - # - # # Using a KeyGenerator instance with a secret and salt(s) - # crypt.rotate secret: old_aead_secret, salt: old_aead_salt, cipher: "aes-256-gcm" - # crypt.rotate secret: old_cbc_secret, salt: old_cbc_salt, signed_salt: old_cbc_signed_salt, cipher: "aes-256-cbc", digest: "SHA1" - # - # # Specifying the key generator instance - # crypt.rotate key_generator: old_key_gen, salt: old_salt, cipher: "aes-256-gcm" + # MessageEncryptor also supports rotating out old configurations by falling + # back to a stack of encryptors. Call `rotate` to build and add an encryptor + # so `decrypt_and_verify` will also try the fallback. + # + # By default any rotated encryptors use the values of the primary + # encryptor unless specified otherwise. + # + # You'd give your encryptor the new defaults: + # + # crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + # + # Then gradually rotate the old values out by adding them as fallbacks. Any message + # generated with the old values will then work until the rotation is removed. + # + # crypt.rotate old_secret # Fallback to an old secret instead of @secret. + # crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm. + # + # Though if both the secret and the cipher was changed at the same time, + # the above should be combined into: + # + # verifier.rotate old_secret, cipher: "aes-256-cbc" class MessageEncryptor prepend Messages::Rotator::Encryptor diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 0be13f6f03..f0b6503b96 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -77,30 +77,27 @@ module ActiveSupport # # === Rotating keys # - # This class also defines a +rotate+ method which can be used to rotate out - # verification keys no longer in use. - # - # This method is called with an options hash where a +:digest+ option and - # either a +:raw_key+ or +:secret+ option must be defined. If +:raw_key+ is - # defined, it is used directly for the underlying HMAC function. If the - # +:secret+ option is defined, a +:salt+ option must also be defined and a - # +KeyGenerator+ instance will be used to derive a key using +:salt+. When - # +:secret+ is used, a +:key_generator+ option may also be defined allowing - # for custom +KeyGenerator+ instances. This method can be called multiple - # times and new verifier instances will be added to the rotation stack on - # each call. - # - # # Specifying the key used for verification - # @verifier.rotate raw_key: older_key, digest: "SHA1" - # - # # Specify the digest - # @verifier.rotate raw_key: old_key, digest: "SHA256" - # - # # Using a KeyGenerator instance with a secret and salt - # @verifier.rotate secret: old_secret, salt: old_salt, digest: "SHA1" - # - # # Specifying the key generator instance - # @verifier.rotate key_generator: old_key_gen, salt: old_salt, digest: "SHA256" + # MessageVerifier also supports rotating out old configurations by falling + # back to a stack of verifiers. Call `rotate` to build and add a verifier to + # so either `verified` or `verify` will also try verifying with the fallback. + # + # By default any rotated verifiers use the values of the primary + # verifier unless specified otherwise. + # + # You'd give your verifier the new defaults: + # + # verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON) + # + # Then gradually rotate the old values out by adding them as fallbacks. Any message + # generated with the old values will then work until the rotation is removed. + # + # verifier.rotate old_secret # Fallback to an old secret instead of @secret. + # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512. + # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON. + # + # Though the above would most likely be combined into one rotation: + # + # verifier.rotate old_secret, digest: "SHA256", serializer: Marshal class MessageVerifier prepend Messages::Rotator::Verifier diff --git a/activesupport/lib/active_support/messages/rotation_configuration.rb b/activesupport/lib/active_support/messages/rotation_configuration.rb index 12566bdb63..bd50d6d348 100644 --- a/activesupport/lib/active_support/messages/rotation_configuration.rb +++ b/activesupport/lib/active_support/messages/rotation_configuration.rb @@ -2,22 +2,19 @@ module ActiveSupport module Messages - class RotationConfiguration + class RotationConfiguration # :nodoc: attr_reader :signed, :encrypted def initialize @signed, @encrypted = [], [] end - def rotate(kind = nil, **options) + def rotate(kind, *args) case kind when :signed - @signed << options + @signed << args when :encrypted - @encrypted << options - else - rotate :signed, options - rotate :encrypted, options + @encrypted << args end end end diff --git a/activesupport/lib/active_support/messages/rotator.rb b/activesupport/lib/active_support/messages/rotator.rb index 21ae643138..823a399d67 100644 --- a/activesupport/lib/active_support/messages/rotator.rb +++ b/activesupport/lib/active_support/messages/rotator.rb @@ -3,14 +3,15 @@ module ActiveSupport module Messages module Rotator # :nodoc: - def initialize(*args) + def initialize(*, **options) super + @options = options @rotations = [] end - def rotate(*args) - @rotations << create_rotation(*args) + def rotate(*secrets, **options) + @rotations << build_rotation(*secrets, @options.merge(options)) end module Encryptor @@ -23,25 +24,8 @@ module ActiveSupport end private - def create_rotation(raw_key: nil, raw_signed_key: nil, **options) - self.class.new \ - raw_key || extract_key(options), - raw_signed_key || extract_signing_key(options), - options.slice(:cipher, :digest, :serializer) - end - - def extract_key(cipher:, salt:, key_generator: nil, secret: nil, **) - key_generator ||= key_generator_for(secret) - key_generator.generate_key(salt, self.class.key_len(cipher)) - end - - def extract_signing_key(cipher:, signed_salt: nil, key_generator: nil, secret: nil, **) - if cipher.downcase.end_with?("cbc") - raise ArgumentError, "missing signed_salt for signing key generation" unless signed_salt - - key_generator ||= key_generator_for(secret) - key_generator.generate_key(signed_salt) - end + def build_rotation(secret = @secret, sign_secret = @sign_secret, options) + self.class.new(secret, sign_secret, options) end end @@ -53,21 +37,12 @@ module ActiveSupport end private - def create_rotation(raw_key: nil, digest: nil, serializer: nil, **options) - self.class.new(raw_key || extract_key(options), digest: digest, serializer: serializer) - end - - def extract_key(key_generator: nil, secret: nil, salt:) - key_generator ||= key_generator_for(secret) - key_generator.generate_key(salt) + def build_rotation(secret = @secret, options) + self.class.new(secret, options) end end private - def key_generator_for(secret) - ActiveSupport::KeyGenerator.new(secret, iterations: 1000) - end - def run_rotations(on_rotation) @rotations.find do |rotation| if message = yield(rotation) rescue next diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb index 51870559ec..b6b31ef140 100644 --- a/activesupport/lib/active_support/security_utils.rb +++ b/activesupport/lib/active_support/security_utils.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "digest" +require "digest/sha2" module ActiveSupport module SecurityUtils diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index b3788ee65c..b878ac20fa 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -399,6 +399,13 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end + def test_indifferent_transform_keys + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k * 2 } + + assert_equal({ "aa" => 1, "bb" => 2 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + def test_indifferent_compact hash_contain_nil_value = @strings.merge("z" => nil) hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 17baf3550b..9edf07f762 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -115,122 +115,70 @@ class MessageEncryptorTest < ActiveSupport::TestCase assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message) end - def test_with_rotated_raw_key - old_raw_key = SecureRandom.random_bytes(32) - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") - old_message = old_encryptor.encrypt_and_sign("message encrypted with old raw key") + def test_rotating_secret + old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").encrypt_and_sign("old") encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") - encryptor.rotate raw_key: old_raw_key, cipher: "aes-256-gcm" + encryptor.rotate secrets[:old] - assert_equal "message encrypted with old raw key", encryptor.decrypt_and_verify(old_message) + assert_equal "old", encryptor.decrypt_and_verify(old_message) end - def test_with_rotated_secret_and_salt - old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" - old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt, 32) + def test_rotating_serializer + old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm", serializer: JSON). + encrypt_and_sign(ahoy: :hoy) - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") - old_message = old_encryptor.encrypt_and_sign("message encrypted with old secret and salt") + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm", serializer: JSON) + encryptor.rotate secrets[:old] - encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") - encryptor.rotate secret: old_secret, salt: old_salt, cipher: "aes-256-gcm" - - assert_equal "message encrypted with old secret and salt", encryptor.decrypt_and_verify(old_message) - end - - def test_with_rotated_key_generator - old_key_gen, old_salt = ActiveSupport::KeyGenerator.new(SecureRandom.random_bytes(32), iterations: 256), "old salt" - - old_raw_key = old_key_gen.generate_key(old_salt, 32) - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") - old_message = old_encryptor.encrypt_and_sign("message encrypted with old key generator and salt") - - encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") - encryptor.rotate key_generator: old_key_gen, salt: old_salt, cipher: "aes-256-gcm" - - assert_equal "message encrypted with old key generator and salt", encryptor.decrypt_and_verify(old_message) + assert_equal({ "ahoy" => "hoy" }, encryptor.decrypt_and_verify(old_message)) end - def test_with_rotated_aes_cbc_encryptor_with_raw_keys - old_raw_key, old_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) - - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") - old_message = old_encryptor.encrypt_and_sign("message encrypted with old raw keys") + def test_rotating_aes_cbc_secrets + old_encryptor = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign", cipher: "aes-256-cbc") + old_message = old_encryptor.encrypt_and_sign("old") encryptor = ActiveSupport::MessageEncryptor.new(@secret) - encryptor.rotate raw_key: old_raw_key, raw_signed_key: old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" + encryptor.rotate secrets[:old], "old sign", cipher: "aes-256-cbc" - assert_equal "message encrypted with old raw keys", encryptor.decrypt_and_verify(old_message) + assert_equal "old", encryptor.decrypt_and_verify(old_message) end - def test_with_rotated_aes_cbc_encryptor_with_secret_and_salts - old_secret, old_salt, old_signed_salt = SecureRandom.random_bytes(32), "old salt", "old signed salt" - - old_key_gen = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000) - old_raw_key = old_key_gen.generate_key(old_salt, 32) - old_raw_signed_key = old_key_gen.generate_key(old_signed_salt) - - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") - old_message = old_encryptor.encrypt_and_sign("message encrypted with old secret and salts") + def test_multiple_rotations + older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign("older") + old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign").encrypt_and_sign("old") encryptor = ActiveSupport::MessageEncryptor.new(@secret) - encryptor.rotate secret: old_secret, salt: old_salt, signed_salt: old_signed_salt, cipher: "aes-256-cbc", digest: "SHA1" + encryptor.rotate secrets[:old], "old sign" + encryptor.rotate secrets[:older], "older sign" - assert_equal "message encrypted with old secret and salts", encryptor.decrypt_and_verify(old_message) + assert_equal "new", encryptor.decrypt_and_verify(encryptor.encrypt_and_sign("new")) + assert_equal "old", encryptor.decrypt_and_verify(old_message) + assert_equal "older", encryptor.decrypt_and_verify(older_message) end - def test_with_rotating_multiple_encryptors - older_raw_key, older_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) - old_raw_key, old_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) - - older_encryptor = ActiveSupport::MessageEncryptor.new(older_raw_key, older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") - older_message = older_encryptor.encrypt_and_sign("message encrypted with older raw key") - - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") - old_message = old_encryptor.encrypt_and_sign("message encrypted with old raw key") + def test_on_rotation_is_called_and_returns_modified_messages + older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign(encoded: "message") encryptor = ActiveSupport::MessageEncryptor.new(@secret) - encryptor.rotate raw_key: old_raw_key, raw_signed_key: old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" - encryptor.rotate raw_key: older_raw_key, raw_signed_key: older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" - - assert_equal "encrypted message", encryptor.decrypt_and_verify(encryptor.encrypt_and_sign("encrypted message")) - assert_equal "message encrypted with old raw key", encryptor.decrypt_and_verify(old_message) - assert_equal "message encrypted with older raw key", encryptor.decrypt_and_verify(older_message) - end + encryptor.rotate secrets[:old] + encryptor.rotate secrets[:older], "older sign" - def test_on_rotation_instance_callback_is_called_and_returns_modified_messages - callback_ran, message = nil, nil + rotated = false + message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = true }) - older_raw_key, older_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) - old_raw_key, old_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) - - older_encryptor = ActiveSupport::MessageEncryptor.new(older_raw_key, older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") - older_message = older_encryptor.encrypt_and_sign(encoded: "message") - - encryptor = ActiveSupport::MessageEncryptor.new(@secret) - encryptor.rotate raw_key: old_raw_key, raw_signed_key: old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" - encryptor.rotate raw_key: older_raw_key, raw_signed_key: older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" - - message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { callback_ran = true }) - - assert callback_ran, "callback was ran" assert_equal({ encoded: "message" }, message) + assert rotated end def test_with_rotated_metadata - old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" - old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt, 32) - - old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") - old_message = old_encryptor.encrypt_and_sign( - "message encrypted with old secret, salt, and metadata", purpose: "rotation") + old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm"). + encrypt_and_sign("metadata", purpose: :rotation) encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") - encryptor.rotate secret: old_secret, salt: old_salt, cipher: "aes-256-gcm" + encryptor.rotate secrets[:old] - assert_equal "message encrypted with old secret, salt, and metadata", - encryptor.decrypt_and_verify(old_message, purpose: "rotation") + assert_equal "metadata", encryptor.decrypt_and_verify(old_message, purpose: :rotation) end private @@ -252,6 +200,10 @@ class MessageEncryptorTest < ActiveSupport::TestCase end end + def secrets + @secrets ||= Hash.new { |h, k| h[k] = SecureRandom.random_bytes(32) } + end + def munge(base64_string) bits = ::Base64.strict_decode64(base64_string) bits.reverse! diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 3079c48c02..05d5c1cbc3 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -92,93 +92,49 @@ class MessageVerifierTest < ActiveSupport::TestCase assert_equal @data, @verifier.verify(signed_message) end - def test_with_rotated_raw_key - old_raw_key = SecureRandom.random_bytes(32) - - old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") - old_message = old_verifier.generate("message verified with old raw key") - - verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") - verifier.rotate raw_key: old_raw_key, digest: "SHA1" - - assert_equal "message verified with old raw key", verifier.verified(old_message) - end - - def test_with_rotated_secret_and_salt - old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" - - old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt) - old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") - old_message = old_verifier.generate("message verified with old secret and salt") + def test_rotating_secret + old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA1").generate("old") verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") - verifier.rotate secret: old_secret, salt: old_salt, digest: "SHA1" + verifier.rotate "old" - assert_equal "message verified with old secret and salt", verifier.verified(old_message) + assert_equal "old", verifier.verified(old_message) end - def test_with_rotated_key_generator - old_key_gen, old_salt = ActiveSupport::KeyGenerator.new(SecureRandom.random_bytes(32), iterations: 256), "old salt" + def test_multiple_rotations + old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA256").generate("old") + older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate("older") - old_raw_key = old_key_gen.generate_key(old_salt) - old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") - old_message = old_verifier.generate("message verified with old key generator and salt") + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512") + verifier.rotate "old", digest: "SHA256" + verifier.rotate "older", digest: "SHA1" - verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") - verifier.rotate key_generator: old_key_gen, salt: old_salt, digest: "SHA1" - - assert_equal "message verified with old key generator and salt", verifier.verified(old_message) + assert_equal "new", verifier.verified(verifier.generate("new")) + assert_equal "old", verifier.verified(old_message) + assert_equal "older", verifier.verified(older_message) end - def test_with_rotating_multiple_verifiers - old_raw_key, older_raw_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(32) - - old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA256") - old_message = old_verifier.generate("message verified with old raw key") + def test_on_rotation_is_called_and_verified_returns_message + older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate(encoded: "message") - older_verifier = ActiveSupport::MessageVerifier.new(older_raw_key, digest: "SHA1") - older_message = older_verifier.generate("message verified with older raw key") + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512") + verifier.rotate "old", digest: "SHA256" + verifier.rotate "older", digest: "SHA1" - verifier = ActiveSupport::MessageVerifier.new("new secret", digest: "SHA512") - verifier.rotate raw_key: old_raw_key, digest: "SHA256" - verifier.rotate raw_key: older_raw_key, digest: "SHA1" + rotated = false + message = verifier.verified(older_message, on_rotation: proc { rotated = true }) - assert_equal "verified message", verifier.verified(verifier.generate("verified message")) - assert_equal "message verified with old raw key", verifier.verified(old_message) - assert_equal "message verified with older raw key", verifier.verified(older_message) - end - - def test_on_rotation_keyword_block_is_called_and_verified_returns_message - callback_ran, message = nil, nil - - old_raw_key, older_raw_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(32) - - older_verifier = ActiveSupport::MessageVerifier.new(older_raw_key, digest: "SHA1") - older_message = older_verifier.generate(encoded: "message") - - verifier = ActiveSupport::MessageVerifier.new("new secret", digest: "SHA512") - verifier.rotate raw_key: old_raw_key, digest: "SHA256" - verifier.rotate raw_key: older_raw_key, digest: "SHA1" - - message = verifier.verified(older_message, on_rotation: proc { callback_ran = true }) - - assert callback_ran, "callback was ran" assert_equal({ encoded: "message" }, message) + assert rotated end - def test_with_rotated_metadata - old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" + def test_rotations_with_metadata + old_message = ActiveSupport::MessageVerifier.new("old").generate("old", purpose: :rotation) - old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt) - old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") - old_message = old_verifier.generate( - "message verified with old secret, salt, and metadata", purpose: "rotation") - - verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") - verifier.rotate secret: old_secret, salt: old_salt, digest: "SHA1" + verifier = ActiveSupport::MessageVerifier.new(@secret) + verifier.rotate "old" - assert_equal "message verified with old secret, salt, and metadata", - verifier.verified(old_message, purpose: "rotation") + assert_equal "old", verifier.verified(old_message, purpose: :rotation) end end diff --git a/activesupport/test/messages/rotation_configuration_test.rb b/activesupport/test/messages/rotation_configuration_test.rb index 41d938e119..2f6824ed21 100644 --- a/activesupport/test/messages/rotation_configuration_test.rb +++ b/activesupport/test/messages/rotation_configuration_test.rb @@ -9,35 +9,17 @@ class MessagesRotationConfiguration < ActiveSupport::TestCase end def test_signed_configurations - @config.rotate :signed, secret: "older secret", salt: "salt", digest: "SHA1" - @config.rotate :signed, secret: "old secret", salt: "salt", digest: "SHA256" + @config.rotate :signed, "older secret", salt: "salt", digest: "SHA1" + @config.rotate :signed, "old secret", salt: "salt", digest: "SHA256" - assert_equal [{ - secret: "older secret", salt: "salt", digest: "SHA1" - }, { - secret: "old secret", salt: "salt", digest: "SHA256" - }], @config.signed + assert_equal [ + [ "older secret", salt: "salt", digest: "SHA1" ], + [ "old secret", salt: "salt", digest: "SHA256" ] ], @config.signed end def test_encrypted_configurations - @config.rotate :encrypted, raw_key: "old raw key", cipher: "aes-256-gcm" + @config.rotate :encrypted, "old raw key", cipher: "aes-256-gcm" - assert_equal [{ - raw_key: "old raw key", cipher: "aes-256-gcm" - }], @config.encrypted - end - - def test_rotate_without_kind - @config.rotate secret: "older secret", salt: "salt", digest: "SHA1" - @config.rotate raw_key: "old raw key", cipher: "aes-256-gcm" - - expected = [{ - secret: "older secret", salt: "salt", digest: "SHA1" - }, { - raw_key: "old raw key", cipher: "aes-256-gcm" - }] - - assert_equal expected, @config.encrypted - assert_equal expected, @config.signed + assert_equal [ [ "old raw key", cipher: "aes-256-gcm" ] ], @config.encrypted end end diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 4d8d8db3e5..341724cdcd 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + # Activate the gem you are reporting the issue against. gem "rails", "5.1.0" end diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 1f862e07da..558d9bf3e2 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" end diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index af777a86ef..013d1f8602 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + # Activate the gem you are reporting the issue against. gem "activejob", "5.1.0" end diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb index 39fb3f60a6..ce480cbb52 100644 --- a/guides/bug_report_templates/active_job_master.rb +++ b/guides/bug_report_templates/active_job_master.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" end diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index 168e2dcc66..921917fbe9 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + # Activate the gem you are reporting the issue against. gem "activerecord", "5.1.0" gem "sqlite3" diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index cbd2cff2b8..78411e2d57 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" gem "sqlite3" diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index b931ed0beb..f75b6fd932 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + # Activate the gem you are reporting the issue against. gem "activerecord", "5.1.0" gem "sqlite3" diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb index 2c009c0563..60416ed42f 100644 --- a/guides/bug_report_templates/active_record_migrations_master.rb +++ b/guides/bug_report_templates/active_record_migrations_master.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" gem "sqlite3" diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb index d0f5a634bc..fb51273e3e 100644 --- a/guides/bug_report_templates/benchmark.rb +++ b/guides/bug_report_templates/benchmark.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" gem "benchmark-ips" diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index c990bda005..60e8322c2a 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + # Activate the gem you are reporting the issue against. gem "activesupport", "5.1.0" end diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index 1a9b99b624..384c8b1833 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -9,6 +9,9 @@ end gemfile(true) do source "https://rubygems.org" + + git_source(:github) { |repo| "https://github.com/#{repo}.git" } + gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" end diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 678b80516f..3573c3c77b 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -414,7 +414,7 @@ end `find_in_batches` works on model classes, as seen above, and also on relations: ```ruby -Invoice.pending.find_in_batches do |invoice| +Invoice.pending.find_in_batches do |invoices| pending_invoices_export.add_invoices(invoices) end ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 86c8364d83..0f87d73d6e 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -493,10 +493,8 @@ Defaults to `'signed cookie'`. * `config.action_dispatch.signed_cookie_digest` sets the digest to be used for signed cookies. This defaults to `"SHA1"`. -* `config.action_dispatch.cookies_rotations` is set to an instance of - [RotationConfiguration](http://api.rubyonrails.org/classes/ActiveSupport/RotationConfiguration.html). - It provides an interface for rotating keys, salts, ciphers, and - digests for encrypted and signed cookies. +* `config.action_dispatch.cookies_rotations` allows rotating + secrets, ciphers, and digests for encrypted and signed cookies. * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) diff --git a/guides/source/engines.md b/guides/source/engines.md index c7331b6ca4..188620a683 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -63,7 +63,7 @@ authentication for its parent applications, or [Thredded](https://github.com/thredded/thredded), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and -[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. +[Refinery CMS](https://github.com/refinery/refinerycms), a CMS engine. Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever @@ -1322,7 +1322,7 @@ engine. Assets within an engine work in an identical way to a full application. Because the engine class inherits from `Rails::Engine`, the application will know to -look up assets in the engine's 'app/assets' and 'lib/assets' directories. +look up assets in the engine's `app/assets` and `lib/assets` directories. Like all of the other components of an engine, the assets should be namespaced. This means that if you have an asset called `style.css`, it should be placed at diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index f46f1648b3..4ce67df93a 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -274,10 +274,12 @@ There are a few things to note here: The resulting HTML is: ```html -<form accept-charset="UTF-8" action="/articles" method="post" class="nifty_form"> - <input id="article_title" name="article[title]" type="text" /> - <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea> - <input name="commit" type="submit" value="Create" /> +<form class="nifty_form" id="new_article" action="/articles" accept-charset="UTF-8" method="post"> + <input name="utf8" type="hidden" value="✓" /> + <input type="hidden" name="authenticity_token" value="NRkFyRWxdYNfUg7vYxLOp2SLf93lvnl+QwDWorR42Dp6yZXPhHEb6arhDOIWcqGit8jfnrPwL781/xlrzj63TA==" /> + <input type="text" name="article[title]" id="article_title" /> + <textarea name="article[body]" id="article_body" cols="60" rows="12"></textarea> + <input type="submit" name="commit" value="Create" data-disable-with="Create" /> </form> ``` @@ -299,9 +301,11 @@ You can create a similar binding without actually creating `<form>` tags with th which produces the following output: ```html -<form accept-charset="UTF-8" action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" /> +<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post"> + <input name="utf8" type="hidden" value="✓" /> + <input type="hidden" name="authenticity_token" value="bL13x72pldyDD8bgtkjKQakJCpd4A8JdXGbfksxBDHdf1uC0kCMqe2tvVdUYfidJt0fj3ihC4NxiVHv8GVYxJA==" /> + <input type="text" name="person[name]" id="person_name" /> + <input type="text" name="contact_detail[phone_number]" id="contact_detail_phone_number" /> </form> ``` diff --git a/guides/source/plugins.md b/guides/source/plugins.md index b3a7f544f5..5048444cb2 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -359,7 +359,7 @@ When you run `bin/test`, you should see the tests all pass: ### Add an Instance Method -This plugin will add a method named 'squawk' to any Active Record object that calls 'acts_as_yaffle'. The 'squawk' +This plugin will add a method named 'squawk' to any Active Record object that calls `acts_as_yaffle`. The 'squawk' method will simply set the value of one of the fields in the database. To start out, write a failing test that shows the behavior you'd like: @@ -392,7 +392,7 @@ end ``` Run the test to make sure the last two tests fail with an error that contains "NoMethodError: undefined method `squawk'", -then update 'acts_as_yaffle.rb' to look like this: +then update `acts_as_yaffle.rb` to look like this: ```ruby # yaffle/lib/yaffle/acts_as_yaffle.rb diff --git a/guides/source/security.md b/guides/source/security.md index b0b71cad7d..a07d583f15 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -152,54 +152,41 @@ In test and development applications get a `secret_key_base` derived from the ap If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret. -### Rotating Keys for Encrypted and Signed Cookies +### Rotating Encrypted and Signed Cookies Configurations -It is possible to rotate the `secret_key_base` as well as the salts, -ciphers, and digests used for both encrypted and signed cookies. Rotating -the `secret_key_base` is necessary if the value was exposed or leaked. -It is also useful to rotate this value for other more benign reasons, -such as an employee leaving your organization or changing hosting -environments. +Rotation is ideal for changing cookie configurations and ensuring old cookies +aren't immediately invalid. Your users then have a chance to visit your site, +get their cookie read with an old configuration and have it rewritten with the +new change. The rotation can then be removed once you're comfortable enough +users have had their chance to get their cookies upgraded. -Key rotations can be defined through the -`config.action_dispatch.cookies_rotations` configuration value. This -value is set to an instance of -[RotationConfiguration](http://api.rubyonrails.org/classes/ActiveSupport/RotationConfiguration.html) -which provides an interface for rotating signed and encrypted cookie -keys, salts, digests, and ciphers. +It's possible to rotate the ciphers and digests used for encrypted and signed cookies. -For example, suppose we want to rotate out an old `secret_key_base`, we -can define a signed and encrypted key rotation as follows: +For instance to change the digest used for signed cookies from SHA1 to SHA256, +you would first assign the new configuration value: ```ruby -config.action_dispatch.cookies_rotations.rotate :encrypted, - cipher: "aes-256-gcm", - secret: Rails.application.credentials.old_secret_key_base, - salt: config.action_dispatch.authenticated_encrypted_cookie_salt - -config.action_dispatch.cookies_rotations.rotate :signed, - digest: "SHA1", - secret: Rails.application.credentials.old_secret_key_base, - salt: config.action_dispatch.signed_cookie_salt +Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256" ``` -Multiple rotations are possible by calling `rotate` multiple times. For -example, suppose we want to use SHA512 for signed cookies while rotating -out SHA256 and SHA1 digests using the same `secret_key_base` +Then you'd set up a rotation with the old configuration to keep it alive. ```ruby -config.action_dispatch.signed_cookie_digest = "SHA512" +Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies| + cookies.rotate :signed, digest: "SHA256" +end +``` -config.action_dispatch.cookies_rotations.rotate :signed, - digest: "SHA256", - secret: Rails.application.credentials.secret_key_base, - salt: config.action_dispatch.signed_cookie_salt +Then any written signed cookies will be digested with SHA256. Old cookies +that were written with SHA1 can still be read, and if accessed will be written +with the new digest so they're upgraded and won't be invalid when you remove the +rotation. -config.action_dispatch.cookies_rotations.rotate :signed, - digest: "SHA1", - secret: Rails.application.credentials.secret_key_base, - salt: config.action_dispatch.signed_cookie_salt -``` +Once users with SHA1 digested signed cookies should no longer have a chance to +have their cookies rewritten, remove the rotation. + +While you can setup as many rotations as you'd like it's not common to have many +rotations going at any one time. For more details on key rotation with encrypted and signed messages as well as the various options the `rotate` method accepts, please refer to diff --git a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb index 5996cb1483..d8eb4f2c7b 100644 --- a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb @@ -5,13 +5,13 @@ require_relative "../../named_base" module Css # :nodoc: module Generators # :nodoc: class ScaffoldGenerator < Rails::Generators::NamedBase # :nodoc: + source_root Rails::Generators::ScaffoldGenerator.source_root + # In order to allow the Sass generators to pick up the default Rails CSS and # transform it, we leave it in a standard location for the CSS stylesheet # generators to handle. For the simple, default case, just copy it over. def copy_stylesheet - dir = Rails::Generators::ScaffoldGenerator.source_root - file = File.join(dir, "scaffold.css") - create_file "app/assets/stylesheets/scaffold.css", File.read(file) + copy_file "scaffold.css", "app/assets/stylesheets/scaffold.css" end end end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 4f2e84f924..0eb9d82bbb 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -1,4 +1,4 @@ -<%%= form_with(model: <%= singular_table_name %>, local: true) do |form| %> +<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %> <%% if <%= singular_table_name %>.errors.any? %> <div id="error_explanation"> <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 5f4904fee1..e1ede7c713 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -18,9 +18,9 @@ <% attributes.reject(&:password_digest?).each do |attribute| -%> <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> <% end -%> - <td><%%= link_to 'Show', <%= singular_table_name %> %></td> - <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td> - <td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td> + <td><%%= link_to 'Show', <%= model_resource_name %> %></td> + <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td> + <td><%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <%% end %> </tbody> @@ -28,4 +28,4 @@ <br> -<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_table_name %>_path %> +<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path %> diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index fe8447be23..5f602f1d52 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -100,11 +100,11 @@ module Rails end def index_helper # :doc: - uncountable? ? "#{plural_table_name}_index" : plural_table_name + uncountable? ? "#{plural_route_name}_index" : plural_route_name end def show_helper # :doc: - "#{singular_table_name}_url(@#{singular_table_name})" + "#{singular_route_name}_url(@#{singular_table_name})" end def edit_helper # :doc: @@ -112,7 +112,7 @@ module Rails end def new_helper # :doc: - "new_#{singular_table_name}_url" + "new_#{singular_route_name}_url" end def field_id(attribute_name) @@ -152,6 +152,35 @@ module Rails end end + def redirect_resource_name # :doc: + model_resource_name(prefix: "@") + end + + def model_resource_name(prefix: "") # :doc: + resource_name = "#{prefix}#{singular_table_name}" + if controller_class_path.empty? + resource_name + else + "[#{controller_class_path.map { |name| ":" + name }.join(", ")}, #{resource_name}]" + end + end + + def singular_route_name # :doc: + if controller_class_path.empty? + singular_table_name + else + "#{controller_class_path.join('_')}_#{singular_table_name}" + end + end + + def plural_route_name # :doc: + if controller_class_path.empty? + plural_table_name + else + "#{controller_class_path.join('_')}_#{plural_table_name}" + end + end + def assign_names!(name) @class_path = name.include?("/") ? name.split("/") : name.split("::") @class_path.map!(&:underscore) diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index ac82ff6633..23fdf03b05 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -69,7 +69,7 @@ module Rails def version_control if !options[:skip_git] && !options[:pretend] - run "git init" + run "git init", capture: options[:quiet] end end @@ -164,7 +164,7 @@ module Rails require_relative "../master_key/master_key_generator" after_bundle do - Rails::Generators::MasterKeyGenerator.new.add_master_key_file + Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet]).add_master_key_file end end @@ -174,7 +174,7 @@ module Rails require_relative "../credentials/credentials_generator" after_bundle do - Rails::Generators::CredentialsGenerator.new.add_credentials_file_silently + Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently end end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt index d744bec32f..70cc71d83b 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -15,6 +15,11 @@ chdir APP_ROOT do puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') +<% unless options.skip_yarn? -%> + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') +<% end -%> <% unless options.skip_active_record? -%> puts "\n== Updating database ==" diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb index e49d3b39e0..395687974a 100644 --- a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb +++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb @@ -13,15 +13,15 @@ module Rails unless MASTER_KEY_PATH.exist? key = ActiveSupport::EncryptedFile.generate_key - say "Adding #{MASTER_KEY_PATH} to store the master 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 anything encrypted with it." + log "Adding #{MASTER_KEY_PATH} to store the master encryption key: #{key}" + log "" + log "Save this in a password manager your team can access." + log "" + log "If you lose the key, no one, including you, can access anything encrypted with it." - say "" + log "" create_file MASTER_KEY_PATH, key - say "" + log "" ignore_master_key_file end @@ -31,15 +31,15 @@ module Rails def ignore_master_key_file if File.exist?(".gitignore") unless File.read(".gitignore").include?(key_ignore) - say "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:" - say "" + log "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:" + log "" append_to_file ".gitignore", key_ignore - say "" + log "" end else - say "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:" - say key_ignore, :on_green - say "" + log "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:" + log key_ignore, :on_green + log "" end end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index 42b9e34274..05f1c2b2d3 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -29,7 +29,7 @@ class <%= controller_class_name %>Controller < ApplicationController @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> if @<%= orm_instance.save %> - redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> + redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully created.'" %> else render :new end @@ -38,7 +38,7 @@ class <%= controller_class_name %>Controller < ApplicationController # PATCH/PUT <%= route_url %>/1 def update if @<%= orm_instance.update("#{singular_table_name}_params") %> - redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> + redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> else render :edit end diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb index 932a6d0e77..ecb4ee3446 100644 --- a/railties/test/application/middleware/cookies_test.rb +++ b/railties/test/application/middleware/cookies_test.rb @@ -52,12 +52,6 @@ module ApplicationTests end test "signed cookies with SHA512 digest and rotated out SHA256 and SHA1 digests" do - key_gen_sha1 = ActiveSupport::KeyGenerator.new("legacy sha1 secret", iterations: 1000) - key_gen_sha256 = ActiveSupport::KeyGenerator.new("legacy sha256 secret", iterations: 1000) - - verifer_sha1 = ActiveSupport::MessageVerifier.new(key_gen_sha1.generate_key("sha1 salt"), digest: :SHA1) - verifer_sha256 = ActiveSupport::MessageVerifier.new(key_gen_sha256.generate_key("sha256 salt"), digest: :SHA256) - app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' @@ -70,12 +64,12 @@ module ApplicationTests protect_from_forgery with: :null_session def write_raw_cookie_sha1 - cookies[:signed_cookie] = "#{verifer_sha1.generate("signed cookie")}" + cookies[:signed_cookie] = TestVerifiers.sha1.generate("signed cookie") head :ok end def write_raw_cookie_sha256 - cookies[:signed_cookie] = "#{verifer_sha256.generate("signed cookie")}" + cookies[:signed_cookie] = TestVerifiers.sha256.generate("signed cookie") head :ok end @@ -90,42 +84,43 @@ module ApplicationTests RUBY add_to_config <<-RUBY - config.action_dispatch.cookies_rotations.rotate :signed, - digest: "SHA1", secret: "legacy sha1 secret", salt: "sha1 salt" + sha1_secret = Rails.application.key_generator.generate_key("sha1") + sha256_secret = Rails.application.key_generator.generate_key("sha256") - config.action_dispatch.cookies_rotations.rotate :signed, - digest: "SHA256", secret: "legacy sha256 secret", salt: "sha256 salt" + ::TestVerifiers = Class.new do + class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1") + class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256") + end config.action_dispatch.signed_cookie_digest = "SHA512" config.action_dispatch.signed_cookie_salt = "sha512 salt" + + config.action_dispatch.cookies_rotations.tap do |cookies| + cookies.rotate :signed, sha1_secret, digest: "SHA1" + cookies.rotate :signed, sha256_secret, digest: "SHA256" + end RUBY require "#{app_path}/config/environment" - verifer_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512) + verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512) get "/foo/write_raw_cookie_sha1" get "/foo/read_signed" assert_equal "signed cookie".inspect, last_response.body get "/foo/read_raw_cookie" - assert_equal "signed cookie", verifer_sha512.verify(last_response.body) + assert_equal "signed cookie", verifier_sha512.verify(last_response.body) get "/foo/write_raw_cookie_sha256" get "/foo/read_signed" assert_equal "signed cookie".inspect, last_response.body get "/foo/read_raw_cookie" - assert_equal "signed cookie", verifer_sha512.verify(last_response.body) + assert_equal "signed cookie", verifier_sha512.verify(last_response.body) end - test "encrypted cookies with multiple rotated out ciphers" do - key_gen_one = ActiveSupport::KeyGenerator.new("legacy secret one", iterations: 1000) - key_gen_two = ActiveSupport::KeyGenerator.new("legacy secret two", iterations: 1000) - - encryptor_one = ActiveSupport::MessageEncryptor.new(key_gen_one.generate_key("salt one", 32), cipher: "aes-256-gcm") - encryptor_two = ActiveSupport::MessageEncryptor.new(key_gen_two.generate_key("salt two", 32), cipher: "aes-256-gcm") - + test "encrypted cookies rotating multiple encryption keys" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' @@ -138,12 +133,12 @@ module ApplicationTests protect_from_forgery with: :null_session def write_raw_cookie_one - cookies[:encrypted_cookie] = "#{encryptor_one.encrypt_and_sign("encrypted cookie")}" + cookies[:encrypted_cookie] = TestEncryptors.first_gcm.encrypt_and_sign("encrypted cookie") head :ok end def write_raw_cookie_two - cookies[:encrypted_cookie] = "#{encryptor_two.encrypt_and_sign("encrypted cookie")}" + cookies[:encrypted_cookie] = TestEncryptors.second_gcm.encrypt_and_sign("encrypted cookie") head :ok end @@ -158,15 +153,22 @@ module ApplicationTests RUBY add_to_config <<-RUBY + first_secret = Rails.application.key_generator.generate_key("first", 32) + second_secret = Rails.application.key_generator.generate_key("second", 32) + + ::TestEncryptors = Class.new do + class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm") + class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm") + end + config.action_dispatch.use_authenticated_cookie_encryption = true config.action_dispatch.encrypted_cookie_cipher = "aes-256-gcm" config.action_dispatch.authenticated_encrypted_cookie_salt = "salt" - config.action_dispatch.cookies_rotations.rotate :encrypted, - cipher: "aes-256-gcm", secret: "legacy secret one", salt: "salt one" - - config.action_dispatch.cookies_rotations.rotate :encrypted, - cipher: "aes-256-gcm", secret: "legacy secret two", salt: "salt two" + config.action_dispatch.cookies_rotations.tap do |cookies| + cookies.rotate :encrypted, first_secret + cookies.rotate :encrypted, second_secret + end RUBY require "#{app_path}/config/environment" diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml b/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml deleted file mode 100644 index fe80872a16..0000000000 --- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml +++ /dev/null @@ -1 +0,0 @@ -# an empty YAML file - any content in here seems to get parsed as a string
\ No newline at end of file diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb deleted file mode 100644 index 1a82a2bdd4..0000000000 --- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -# intentionally empty diff --git a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb deleted file mode 100644 index 1a82a2bdd4..0000000000 --- a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -# intentionally empty diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 904e2a5c84..20f593f25c 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -560,6 +560,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/run git init/, output) end + def test_quiet_option + output = run_generator [File.join(destination_root, "myapp"), "--quiet"] + assert_empty output + end + def test_application_name_with_spaces path = File.join(destination_root, "foo bar") @@ -737,7 +742,7 @@ class AppGeneratorTest < Rails::Generators::TestCase sequence = ["git init", "install", "exec spring binstub --all", "echo ran after_bundle"] @sequence_step ||= 0 - ensure_bundler_first = -> command do + ensure_bundler_first = -> command, options = nil do assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" @sequence_step += 1 end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 67f05926e3..64e9909859 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -131,6 +131,19 @@ class NamedBaseTest < Rails::Generators::TestCase assert_name g, "admin/foos", :controller_file_path assert_name g, "foos", :controller_file_name assert_name g, "admin.foos", :controller_i18n_scope + assert_name g, "admin_user", :singular_route_name + assert_name g, "admin_users", :plural_route_name + assert_name g, "[:admin, @user]", :redirect_resource_name + assert_name g, "[:admin, user]", :model_resource_name + assert_name g, "admin_users", :index_helper + end + + def test_scaffold_plural_names + g = generator ["User"] + assert_name g, "@user", :redirect_resource_name + assert_name g, "user", :model_resource_name + assert_name g, "user", :singular_route_name + assert_name g, "users", :plural_route_name end private diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 384524aba9..513b037043 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -174,6 +174,29 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_instance_method :index, content do |m| assert_match("@users = User.all", m) end + + assert_instance_method :create, content do |m| + assert_match("redirect_to [:admin, @user]", m) + end + + assert_instance_method :update, content do |m| + assert_match("redirect_to [:admin, @user]", m) + end + end + + assert_file "app/views/admin/users/index.html.erb" do |content| + assert_match("'Show', [:admin, user]", content) + assert_match("'Edit', edit_admin_user_path(user)", content) + assert_match("'Destroy', [:admin, user]", content) + assert_match("'New User', new_admin_user_path", content) + end + + assert_file "app/views/admin/users/new.html.erb" do |content| + assert_match("'Back', admin_users_path", content) + end + + assert_file "app/views/admin/users/_form.html.erb" do |content| + assert_match("model: [:admin, user]", content) end end diff --git a/tasks/release.rb b/tasks/release.rb index aa8ba44c1a..6ff06f3c4a 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -6,7 +6,6 @@ FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitali root = File.expand_path("..", __dir__) version = File.read("#{root}/RAILS_VERSION").strip tag = "v#{version}" -gem_version = Gem::Version.new(version) directory "pkg" |