diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index ba48358879..3b1986f4f7 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,16 @@ +* Fix the `Digest::UUID.uuid_from_hash` behavior for namespace IDs that are different from the ones defined on `Digest::UUID`. + + The new behavior will be enabled by setting the + `config.active_support.use_rfc4122_namespaced_uuids` option to `true` + and is the default for new apps. + + The old behavior is the default for upgraded apps and will output a + deprecation warning every time a value that is different than one of + the constants defined on the `Digest::UUID` extension is used as the + namespace ID. + + *Alex Robbin, Erich Soares Machado, Eugene Kenny* + * `ActiveSupport::Inflector::Inflections#clear(:acronyms)` is now supported, and `inflector.clear` / `inflector.clear(:all)` also clears acronyms. diff --git a/activesupport/lib/active_support/core_ext/digest.rb b/activesupport/lib/active_support/core_ext/digest.rb index ce1427e13a..a226bf2146 100644 --- a/activesupport/lib/active_support/core_ext/digest.rb +++ b/activesupport/lib/active_support/core_ext/digest.rb @@ -1,3 +1,9 @@ # frozen_string_literal: true require "active_support/core_ext/digest/uuid" + +module ActiveSupport + class << self + delegate :use_rfc4122_namespaced_uuids, :use_rfc4122_namespaced_uuids=, to: :'Digest::UUID' + end +end diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb index 000451f1cf..3546932c4f 100644 --- a/activesupport/lib/active_support/core_ext/digest/uuid.rb +++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb @@ -10,13 +10,15 @@ module Digest OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc: X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" # :nodoc: + mattr_accessor :use_rfc4122_namespaced_uuids, instance_accessor: false, default: false + # Generates a v5 non-random UUID (Universally Unique IDentifier). # # Using OpenSSL::Digest::MD5 generates version 3 UUIDs; OpenSSL::Digest::SHA1 generates version 5 UUIDs. # uuid_from_hash always generates the same UUID for a given name and namespace combination. # # See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt - def self.uuid_from_hash(hash_class, uuid_namespace, name) + def self.uuid_from_hash(hash_class, namespace, name) if hash_class == Digest::MD5 || hash_class == OpenSSL::Digest::MD5 version = 3 elsif hash_class == Digest::SHA1 || hash_class == OpenSSL::Digest::SHA1 @@ -25,6 +27,8 @@ module Digest raise ArgumentError, "Expected OpenSSL::Digest::SHA1 or OpenSSL::Digest::MD5, got #{hash_class.name}." end + uuid_namespace = pack_uuid_namespace(namespace) + hash = hash_class.new hash.update(uuid_namespace) hash.update(name) @@ -50,5 +54,26 @@ module Digest def self.uuid_v4 SecureRandom.uuid end + + def self.pack_uuid_namespace(namespace) + if [DNS_NAMESPACE, OID_NAMESPACE, URL_NAMESPACE, X500_NAMESPACE].include?(namespace) + namespace + elsif use_rfc4122_namespaced_uuids == true + match_data = namespace.match(/\A(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})\z/) + + raise ArgumentError, "Only UUIDs are valid namespace identifiers" unless match_data.present? + + match_data.captures.map { |s| s.to_i(16) }.pack("NnnnnN") + else + ActiveSupport::Deprecation.warn <<~WARNING.squish + Providing a namespace ID that is not one of the constants defined on Digest::UUID generates an incorrect UUID value according to RFC 4122. + To enable the correct behavior, set the Rails.application.config.active_support.use_rfc4122_namespaced_uuids configuration option to true. + WARNING + + namespace + end + end + + private_class_method :pack_uuid_namespace end end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 8d524a2037..76ff7cd28e 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -121,5 +121,14 @@ module ActiveSupport end end end + + initializer "active_support.set_rfc4122_namespaced_uuids" do |app| + config.after_initialize do + if app.config.active_support.use_rfc4122_namespaced_uuids + require "active_support/core_ext/digest" + ActiveSupport.use_rfc4122_namespaced_uuids = app.config.active_support.use_rfc4122_namespaced_uuids + end + end + end end end diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb index 1dc21bb2dc..8749b30f42 100644 --- a/activesupport/test/core_ext/digest/uuid_test.rb +++ b/activesupport/test/core_ext/digest/uuid_test.rb @@ -1,21 +1,182 @@ # frozen_string_literal: true require_relative "../../abstract_unit" -require "active_support/core_ext/digest/uuid" +require "active_support/core_ext/digest" class DigestUUIDExt < ActiveSupport::TestCase - def test_v3_uuids - assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") - assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") - assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3") - assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + def with_use_rfc4122_namespaced_uuids_set + old_value = ActiveSupport.use_rfc4122_namespaced_uuids + ActiveSupport.use_rfc4122_namespaced_uuids = true + yield + ensure + ActiveSupport.use_rfc4122_namespaced_uuids = old_value end - def test_v5_uuids - assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") - assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") - assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3") - assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + def test_constants + assert_equal "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "%08x-%04x-%04x-%04x-%04x%08x" % Digest::UUID::DNS_NAMESPACE.unpack("NnnnnN") + assert_equal "6ba7b811-9dad-11d1-80b4-00c04fd430c8", "%08x-%04x-%04x-%04x-%04x%08x" % Digest::UUID::URL_NAMESPACE.unpack("NnnnnN") + assert_equal "6ba7b812-9dad-11d1-80b4-00c04fd430c8", "%08x-%04x-%04x-%04x-%04x%08x" % Digest::UUID::OID_NAMESPACE.unpack("NnnnnN") + assert_equal "6ba7b814-9dad-11d1-80b4-00c04fd430c8", "%08x-%04x-%04x-%04x-%04x%08x" % Digest::UUID::X500_NAMESPACE.unpack("NnnnnN") + end + + def test_v3_uuids_with_rfc4122_namespaced_uuids_enabled + with_use_rfc4122_namespaced_uuids_set do + assert_not_deprecated do + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3("6BA7B810-9DAD-11D1-80B4-00C04FD430C8", "www.widgets.com") + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "www.widgets.com") + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3("6BA7B811-9DAD-11D1-80B4-00C04FD430C8", "http://www.widgets.com") + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3("6ba7b811-9dad-11d1-80b4-00c04fd430c8", "http://www.widgets.com") + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3("6BA7B812-9DAD-11D1-80B4-00C04FD430C8", "1.2.3") + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3("6ba7b812-9dad-11d1-80b4-00c04fd430c8", "1.2.3") + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3") + + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3("6BA7B814-9DAD-11D1-80B4-00C04FD430C8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3("6ba7b814-9dad-11d1-80b4-00c04fd430c8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_raise ArgumentError do + Digest::UUID.uuid_v3("A non-UUID string", "some value") + end + end + end + + def test_v3_uuids_with_rfc4122_namespaced_uuids_disabled + assert_deprecated do + assert_equal "995e5d8e-364a-386e-8b3d-65d6a7d5478f", Digest::UUID.uuid_v3("6BA7B810-9DAD-11D1-80B4-00C04FD430C8", "www.widgets.com") + end + + assert_deprecated do + assert_equal "fe5a52d1-703f-3326-b919-2d96003288f3", Digest::UUID.uuid_v3("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "www.widgets.com") + end + + assert_not_deprecated do + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + end + + assert_deprecated do + assert_equal "1a27509f-2955-3d78-8f53-c92935fecc57", Digest::UUID.uuid_v3("6BA7B811-9DAD-11D1-80B4-00C04FD430C8", "http://www.widgets.com") + end + + assert_deprecated do + assert_equal "2676127a-9073-36e3-b9db-14bc16b7c083", Digest::UUID.uuid_v3("6ba7b811-9dad-11d1-80b4-00c04fd430c8", "http://www.widgets.com") + end + + assert_not_deprecated do + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + end + + assert_deprecated do + assert_equal "2e2a2437-160c-36e7-952d-d6f494edea44", Digest::UUID.uuid_v3("6BA7B812-9DAD-11D1-80B4-00C04FD430C8", "1.2.3") + end + + assert_deprecated do + assert_equal "719357e1-54f1-3930-8113-a1faffde48fa", Digest::UUID.uuid_v3("6ba7b812-9dad-11d1-80b4-00c04fd430c8", "1.2.3") + end + + assert_not_deprecated do + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3") + end + + assert_deprecated do + assert_equal "01c2671b-fd20-3e43-8cff-217f40e110c8", Digest::UUID.uuid_v3("6BA7B814-9DAD-11D1-80B4-00C04FD430C8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_deprecated do + assert_equal "32560c4a-c9f1-3974-9c1c-5e52761e091f", Digest::UUID.uuid_v3("6ba7b814-9dad-11d1-80b4-00c04fd430c8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_not_deprecated do + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_deprecated do + assert_equal "cd3d768f-7380-3d1f-8834-e034b40e65ea", Digest::UUID.uuid_v3("A non-UUID string", "some value") + end + end + + def test_v5_uuids_with_rfc4122_namespaced_uuids_enabled + with_use_rfc4122_namespaced_uuids_set do + assert_not_deprecated do + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5("6BA7B810-9DAD-11D1-80B4-00C04FD430C8", "www.widgets.com") + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "www.widgets.com") + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5("6BA7B811-9DAD-11D1-80B4-00C04FD430C8", "http://www.widgets.com") + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5("6ba7b811-9dad-11d1-80b4-00c04fd430c8", "http://www.widgets.com") + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5("6BA7B812-9DAD-11D1-80B4-00C04FD430C8", "1.2.3") + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5("6ba7b812-9dad-11d1-80b4-00c04fd430c8", "1.2.3") + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3") + + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5("6BA7B814-9DAD-11D1-80B4-00C04FD430C8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5("6ba7b814-9dad-11d1-80b4-00c04fd430c8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_raise ArgumentError do + Digest::UUID.uuid_v5("A non-UUID string", "some value") + end + end + end + + def test_v5_uuids_with_rfc4122_namespaced_uuids_disabled + assert_deprecated do + assert_equal "442faf6c-4996-5266-aeef-ecadb5d49e54", Digest::UUID.uuid_v5("6BA7B810-9DAD-11D1-80B4-00C04FD430C8", "www.widgets.com") + end + + assert_deprecated do + assert_equal "027963ef-431c-5670-ab2c-820168da74e9", Digest::UUID.uuid_v5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "www.widgets.com") + end + + assert_not_deprecated do + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + end + + assert_deprecated do + assert_equal "59207e54-33c5-5914-ab39-b7f3333a0097", Digest::UUID.uuid_v5("6BA7B811-9DAD-11D1-80B4-00C04FD430C8", "http://www.widgets.com") + end + + assert_deprecated do + assert_equal "d8e1e518-2337-58e5-bf52-6c563631db90", Digest::UUID.uuid_v5("6ba7b811-9dad-11d1-80b4-00c04fd430c8", "http://www.widgets.com") + end + + assert_not_deprecated do + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + end + + assert_deprecated do + assert_equal "72409eff-7406-5906-b86e-6c7a726ed04e", Digest::UUID.uuid_v5("6BA7B812-9DAD-11D1-80B4-00C04FD430C8", "1.2.3") + end + + assert_deprecated do + assert_equal "b9b86653-48bb-5059-861a-2c72974b5c8d", Digest::UUID.uuid_v5("6ba7b812-9dad-11d1-80b4-00c04fd430c8", "1.2.3") + end + + assert_not_deprecated do + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3") + end + + assert_deprecated do + assert_equal "de6fe50e-eded-580a-81c9-f0774a3531da", Digest::UUID.uuid_v5("6BA7B814-9DAD-11D1-80B4-00C04FD430C8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_deprecated do + assert_equal "e84a8a4e-a5c7-55b8-ad09-020c0b5662a7", Digest::UUID.uuid_v5("6ba7b814-9dad-11d1-80b4-00c04fd430c8", "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_not_deprecated do + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + assert_deprecated do + assert_equal "b42d5423-1047-5bb3-afd4-0dec60fb22d2", Digest::UUID.uuid_v5("A non-UUID string", "some value") + end end def test_invalid_hash_class diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 3ec05a292e..e484407614 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -1385,6 +1385,26 @@ Configures deprecation warnings that the Application considers disallowed. This Allows you to disable all deprecation warnings (including disallowed deprecations); it makes `ActiveSupport::Deprecation.warn` a no-op. This is enabled by default in production. +#### `config.active_support.use_rfc4122_namespaced_uuids` + +Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls. + +If set to `true`: + +* Only UUIDs are allowed as namespace IDs. If a namespace ID value provided is not allowed, an `ArgumentError` will be raised. +* No deprecation warning will be generated, no matter if the namespace ID used is one of the constants defined on `Digest::UUID` or a `String`. +* Namespace IDs are case-insensitive. +* All generated namespaced UUIDs should be compliant to the standard. + +If set to `false`: + +* Any `String` value can be used as namespace ID (although not recommended). No `ArgumentError` will be raised in this case in order to preserve backwards-compatibility. +* A deprecation warning will be generated if the namespace ID provided is not one of the constants defined on `Digest::UUID`. +* Namespace IDs are case-sensitive. +* Only namespaced UUIDs generated using one of the namespace ID constants defined on `Digest::UUID` are compliant to the standard. + +The default value is `true` for new apps. Upgraded apps will have that value set to `false` for backwards-compatibility. + #### `ActiveSupport::Logger.silencer` Is set to `false` to disable the ability to silence logging in a block. The default is `true`. diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 99472cf410..4d4ac85516 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -213,6 +213,7 @@ module Rails active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256 active_support.remove_deprecated_time_with_zone_name = true active_support.cache_format_version = 7.0 + active_support.use_rfc4122_namespaced_uuids = true end if respond_to?(:action_mailer) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index eba2ad69d3..159b71f779 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -2422,6 +2422,20 @@ module ApplicationTests assert_equal 1234, ActiveSupport.test_parallelization_threshold end + test "ActiveSupport.use_rfc4122_namespaced_uuids is enabled by default for new apps" do + app "development" + + assert_equal true, ActiveSupport.use_rfc4122_namespaced_uuids + end + + test "ActiveSupport.use_rfc4122_namespaced_uuids is disabled by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal false, ActiveSupport.use_rfc4122_namespaced_uuids + end + test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end