aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport')
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb6
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb9
-rw-r--r--activesupport/lib/active_support/parameter_filter.rb7
-rw-r--r--activesupport/lib/active_support/secure_compare_rotator.rb52
-rw-r--r--activesupport/test/core_ext/module_test.rb29
-rw-r--r--activesupport/test/parameter_filter_test.rb7
-rw-r--r--activesupport/test/secure_compare_rotator_test.rb44
-rw-r--r--activesupport/test/transliterate_test.rb8
8 files changed, 146 insertions, 16 deletions
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 54271a3970..14d7f0c484 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -276,6 +276,11 @@ class Module
# The delegated method must be public on the target, otherwise it will
# raise +DelegationError+. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
+ #
+ # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
+ # delegation due to possible interference when calling
+ # <tt>Marshal.dump(object)</tt>, should the delegation target method
+ # of <tt>object</tt> add or remove instance variables.
def delegate_missing_to(target, allow_nil: nil)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
@@ -285,6 +290,7 @@ class Module
# It may look like an oversight, but we deliberately do not pass
# +include_private+, because they do not get delegated.
+ return false if name == :marshal_dump || name == :_dump
#{target}.respond_to?(name) || super
end
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index ea7161a6ba..ec6e9ccb59 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -5,9 +5,8 @@ require "active_support/i18n"
module ActiveSupport
module Inflector
- # Replaces non-ASCII characters in a UTF-8 encoded string with an ASCII
- # approximation, or if none exists, a replacement character which
- # defaults to "?".
+ # Replaces non-ASCII characters with an ASCII approximation, or if none
+ # exists, a replacement character which defaults to "?".
#
# transliterate('Ærøskøbing')
# # => "AEroskobing"
@@ -57,12 +56,8 @@ module ActiveSupport
#
# transliterate('Jürgen', locale: :de)
# # => "Juergen"
- #
- # This method requires that `string` be UTF-8 encoded. Passing an argument
- # with a different string encoding will raise an ArgumentError.
def transliterate(string, replacement = "?", locale: nil)
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
- raise ArgumentError, "Can only transliterate UTF-8 strings. Received string with encoding #{string.encoding}" unless string.encoding == ::Encoding::UTF_8
I18n.transliterate(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb
index 8e5595babf..e1cd7c46c1 100644
--- a/activesupport/lib/active_support/parameter_filter.rb
+++ b/activesupport/lib/active_support/parameter_filter.rb
@@ -109,7 +109,12 @@ module ActiveSupport
elsif value.is_a?(Hash)
value = call(value, parents, original_params)
elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v }
+ # If we don't pop the current parent it will be duplicated as we
+ # process each array value.
+ parents.pop if deep_regexps
+ value = value.map { |v| value_for_key(key, v, parents, original_params) }
+ # Restore the parent stack after processing the array.
+ parents.push(key) if deep_regexps
elsif blocks.any?
key = key.dup if key.duplicable?
value = value.dup if value.duplicable?
diff --git a/activesupport/lib/active_support/secure_compare_rotator.rb b/activesupport/lib/active_support/secure_compare_rotator.rb
new file mode 100644
index 0000000000..14a0aee947
--- /dev/null
+++ b/activesupport/lib/active_support/secure_compare_rotator.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "active_support/security_utils"
+require "active_support/messages/rotator"
+
+module ActiveSupport
+ # The ActiveSupport::SecureCompareRotator is a wrapper around +ActiveSupport::SecurityUtils.secure_compare+
+ # and allows you to rotate a previously defined value to a new one.
+ #
+ # It can be used as follow:
+ #
+ # rotator = ActiveSupport::SecureCompareRotator.new('new_production_value')
+ # rotator.rotate('previous_production_value')
+ # rotator.secure_compare!('previous_production_value')
+ #
+ # One real use case example would be to rotate a basic auth credentials:
+ #
+ # class MyController < ApplicationController
+ # def authenticate_request
+ # rotator = ActiveSupport::SecureComparerotator.new('new_password')
+ # rotator.rotate('old_password')
+ #
+ # authenticate_or_request_with_http_basic do |username, password|
+ # rotator.secure_compare!(password)
+ # rescue ActiveSupport::SecureCompareRotator::InvalidMatch
+ # false
+ # end
+ # end
+ # end
+ class SecureCompareRotator
+ include SecurityUtils
+ prepend Messages::Rotator
+
+ InvalidMatch = Class.new(StandardError)
+
+ def initialize(value, **_options)
+ @value = value
+ end
+
+ def secure_compare!(other_value, on_rotation: @rotation)
+ secure_compare(@value, other_value) ||
+ run_rotations(on_rotation) { |wrapper| wrapper.secure_compare!(other_value) } ||
+ raise(InvalidMatch)
+ end
+
+ private
+
+ def build_rotation(previous_value, _options)
+ self.class.new(previous_value)
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index ec9ecd06ee..dd36a9373a 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -111,6 +111,24 @@ class DecoratedReserved
end
end
+class Maze
+ attr_accessor :cavern, :passages
+end
+
+class Cavern
+ delegate_missing_to :target
+
+ attr_reader :maze
+
+ def initialize(maze)
+ @maze = maze
+ end
+
+ def target
+ @maze.passages = :twisty
+ end
+end
+
class Block
def hello?
true
@@ -411,6 +429,17 @@ class ModuleTest < ActiveSupport::TestCase
assert_respond_to DecoratedTester.new(@david), :extra_missing
end
+ def test_delegate_missing_to_does_not_interfere_with_marshallization
+ maze = Maze.new
+ maze.cavern = Cavern.new(maze)
+
+ array = [maze, nil]
+ serialized_array = Marshal.dump(array)
+ deserialized_array = Marshal.load(serialized_array)
+
+ assert_nil deserialized_array[1]
+ end
+
def test_delegate_with_case
event = Event.new(Tester.new)
assert_equal 1, event.foo
diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb
index d2dc71061d..e680a22479 100644
--- a/activesupport/test/parameter_filter_test.rb
+++ b/activesupport/test/parameter_filter_test.rb
@@ -28,10 +28,17 @@ class ParameterFilterTest < ActiveSupport::TestCase
value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
}
+ filter_words << lambda { |key, value|
+ value.upcase! if key == "array_elements"
+ }
+
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } }
+ before_filter["array_elements"] = %w(element1 element2)
+ after_filter["array_elements"] = %w(ELEMENT1 ELEMENT2)
+
assert_equal after_filter, parameter_filter.filter(before_filter)
end
end
diff --git a/activesupport/test/secure_compare_rotator_test.rb b/activesupport/test/secure_compare_rotator_test.rb
new file mode 100644
index 0000000000..8acf13e38f
--- /dev/null
+++ b/activesupport/test/secure_compare_rotator_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/secure_compare_rotator"
+
+class SecureCompareRotatorTest < ActiveSupport::TestCase
+ test "#secure_compare! works correctly after rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+
+ assert_equal(true, wrapper.secure_compare!("new_secret"))
+ end
+
+ test "#secure_compare! works correctly after multiple rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ assert_equal(true, wrapper.secure_compare!("and_another_one"))
+ end
+
+ test "#secure_compare! fails correctly when credential is not part of the rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+
+ assert_raises(ActiveSupport::SecureCompareRotator::InvalidMatch) do
+ wrapper.secure_compare!("different_secret")
+ end
+ end
+
+ test "#secure_compare! calls the on_rotation proc" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ @witness = nil
+
+ assert_changes(:@witness, from: nil, to: true) do
+ assert_equal(true, wrapper.secure_compare!("and_another_one", on_rotation: -> { @witness = true }))
+ end
+ end
+end
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index 525b4a8559..9e29a93ea0 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -57,12 +57,4 @@ class TransliterateTest < ActiveSupport::TestCase
end
assert_equal "Can only transliterate strings. Received Object", exception.message
end
-
- def test_transliterate_handles_non_unicode_strings
- ascii_8bit_string = "A".b
- exception = assert_raises ArgumentError do
- assert_equal "A", ActiveSupport::Inflector.transliterate(ascii_8bit_string)
- end
- assert_equal "Can only transliterate UTF-8 strings. Received string with encoding ASCII-8BIT", exception.message
- end
end