From 0e1e416e04f88baa3fdf4a6b2e171f749d41f4e6 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 13 Jun 2023 18:25:52 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A=20Move=20SASL.authenticator=20regi?= =?UTF-8?q?stry=20from=20Net::IMAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original methods (`Net::IMAP.authenticator` and `Net::IMAP.add_authenticator`) both still work, by delegation to the new methods. But they have been deprecated and will issue warnings. --- lib/net/imap.rb | 5 +- lib/net/imap/authenticators.rb | 76 +++++-------------- lib/net/imap/sasl.rb | 13 ++++ lib/net/imap/sasl/authenticators.rb | 92 +++++++++++++++++++++++ test/net/imap/test_imap_authenticators.rb | 22 ++++-- 5 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 lib/net/imap/sasl/authenticators.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 2f5d320c..43f4b477 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1074,10 +1074,7 @@ def starttls(options = {}, verify = true) # completes. If the TaggedResponse to #authenticate includes updated # capabilities, they will be cached. def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback) - authenticator = self.class.authenticator(mechanism, - *creds, - **props, - &callback) + authenticator = SASL.authenticator(mechanism, *creds, **props, &callback) cmdargs = ["AUTHENTICATE", mechanism] if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) && SASL.initial_response?(authenticator) diff --git a/lib/net/imap/authenticators.rb b/lib/net/imap/authenticators.rb index fda1c4e9..8ecb3851 100644 --- a/lib/net/imap/authenticators.rb +++ b/lib/net/imap/authenticators.rb @@ -1,67 +1,31 @@ # frozen_string_literal: true -# Registry for SASL authenticators used by Net::IMAP. +# Backward compatible delegators from Net::IMAP to Net::IMAP::SASL. module Net::IMAP::Authenticators - # Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the - # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml] - # implemented by +authenticator+ (for instance, "PLAIN"). - # - # The +authenticator+ must respond to +#new+ (or #call), receiving the - # authenticator configuration and return a configured authentication session. - # The authenticator session must respond to +#process+, receiving the server's - # challenge and returning the client's response. - # - # See PlainAuthenticator, XOAuth2Authenticator, and DigestMD5Authenticator for - # examples. - def add_authenticator(auth_type, authenticator) - authenticators[auth_type] = authenticator + # Deprecated. Use Net::IMAP::SASL.add_authenticator instead. + def add_authenticator(...) + warn( + "%s.%s is deprecated. Use %s.%s instead." % [ + Net::IMAP, __method__, Net::IMAP::SASL, __method__ + ], + uplevel: 1 + ) + Net::IMAP::SASL.add_authenticator(...) end - # :call-seq: - # authenticator(mechanism, ...) -> authenticator - # authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator - # authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator - # authenticator(mechanism, **properties) -> authenticator - # authenticator(mechanism) {|propname, authctx| value } -> authenticator - # - # Builds a new authentication session context for +mechanism+. - # - # [Note] - # This method is intended for internal use by connection protocol code only. - # Protocol client users should see refer to their client's documentation, - # e.g. Net::IMAP#authenticate for Net::IMAP. - # - # The call signatures documented for this method are recommendations for - # authenticator implementors. All arguments (other than +mechanism+) are - # forwarded to the registered authenticator's +#new+ (or +#call+) method, and - # each authenticator must document its own arguments. - # - # The returned object represents a single authentication exchange and must - # not be reused for multiple authentication attempts. - def authenticator(mechanism, ...) - auth = authenticators.fetch(mechanism.upcase) do - raise ArgumentError, 'unknown auth type - "%s"' % mechanism - end - auth.respond_to?(:new) ? auth.new(...) : auth.call(...) + # Deprecated. Use Net::IMAP::SASL.authenticator instead. + def authenticator(...) + warn( + "%s.%s is deprecated. Use %s.%s instead." % [ + Net::IMAP, __method__, Net::IMAP::SASL, __method__ + ], + uplevel: 1 + ) + Net::IMAP::SASL.authenticator(...) end - private - - def authenticators - @authenticators ||= {} - end - -end - -class Net::IMAP - extend Authenticators - add_authenticator "PLAIN", SASL::PlainAuthenticator - add_authenticator "XOAUTH2", SASL::XOAuth2Authenticator - - add_authenticator "CRAM-MD5", SASL::CramMD5Authenticator # deprecated - add_authenticator "LOGIN", SASL::LoginAuthenticator # deprecated - add_authenticator "DIGEST-MD5", SASL::DigestMD5Authenticator # deprecated + Net::IMAP.extend self end class Net::IMAP diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb index eaf5f0b2..6f2db923 100644 --- a/lib/net/imap/sasl.rb +++ b/lib/net/imap/sasl.rb @@ -33,6 +33,8 @@ module SASL autoload :BidiStringError, sasl_stringprep_rb sasl_dir = File.expand_path("sasl", __dir__) + autoload :Authenticators, "#{sasl_dir}/authenticators" + autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator" autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator" @@ -40,6 +42,17 @@ module SASL autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator" autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator" + # Returns the default global SASL::Authenticators instance. + def self.authenticators + @authenticators ||= Authenticators.new(use_defaults: true) + end + + # Delegates to ::authenticators. See Authenticators#authenticator. + def self.authenticator(...) authenticators.authenticator(...) end + + # Delegates to ::authenticators. See Authenticators#add_authenticator. + def self.add_authenticator(...) authenticators.add_authenticator(...) end + module_function # See Net::IMAP::StringPrep::SASLprep#saslprep. diff --git a/lib/net/imap/sasl/authenticators.rb b/lib/net/imap/sasl/authenticators.rb new file mode 100644 index 00000000..779a8018 --- /dev/null +++ b/lib/net/imap/sasl/authenticators.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Net::IMAP::SASL + + # Registry for SASL authenticators + # + # Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or + # a proc), receiving any credentials and options and returning an + # authenticator instance. The returned object represents a single + # authentication exchange and must not be reused for multiple + # authentication attempts. + # + # An authenticator instance object must respond to +#process+, receiving the + # server's challenge and returning the client's response. Optionally, it may + # also respond to +#initial_response?+ and +#done?+. When + # +#initial_response?+ returns +true+, +#process+ may be called the first + # time with +nil+. When +#done?+ returns +false+, the exchange is incomplete + # and an exception should be raised if the exchange terminates prematurely. + # + # See the source for PlainAuthenticator, XOAuth2Authenticator, and + # ScramSHA1Authenticator for examples. + class Authenticators + + # Create a new Authenticators registry. + # + # This class is usually not instantiated directly. Use SASL.authenticators + # to reuse the default global registry. + # + # By default, the registry will be empty--without any registrations. When + # +add_defaults+ is +true+, authenticators for all standard mechanisms will + # be registered. + # + def initialize(use_defaults: false) + @authenticators = {} + if use_defaults + add_authenticator "PLAIN", PlainAuthenticator + add_authenticator "XOAUTH2", XOAuth2Authenticator + add_authenticator "LOGIN", LoginAuthenticator # deprecated + add_authenticator "CRAM-MD5", CramMD5Authenticator # deprecated + add_authenticator "DIGEST-MD5", DigestMD5Authenticator # deprecated + end + end + + # Returns the names of all registered SASL mechanisms. + def names; @authenticators.keys end + + # :call-seq: + # add_authenticator(mechanism) + # add_authenticator(mechanism, authenticator_class) + # add_authenticator(mechanism, authenticator_proc) + # + # Registers an authenticator for #authenticator to use. +mechanism+ is the + # name of the + # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml] + # implemented by +authenticator_class+ (for instance, "PLAIN"). + # + # If +mechanism+ refers to an existing authenticator, a warning will be + # printed and the old authenticator will be replaced. + # + # When only a single argument is given, the authenticator class will be + # lazily loaded from Net::IMAP::SASL::#{name}Authenticator (case is + # preserved and non-alphanumeric characters are removed.. + def add_authenticator(auth_type, authenticator) + @authenticators[auth_type] = authenticator + end + + # :call-seq: + # authenticator(mechanism, ...) -> auth_session + # + # Builds an authenticator instance using the authenticator registered to + # +mechanism+. The returned object represents a single authentication + # exchange and must not be reused for multiple authentication + # attempts. + # + # All arguments (except +mechanism+) are forwarded to the registered + # authenticator's +#new+ or +#call+ method. Each authenticator must + # document its own arguments. + # + # [Note] + # This method is intended for internal use by connection protocol code + # only. Protocol client users should see refer to their client's + # documentation, e.g. Net::IMAP#authenticate. + def authenticator(mechanism, ...) + auth = @authenticators.fetch(mechanism.upcase) do + raise ArgumentError, 'unknown auth type - "%s"' % mechanism + end + auth.respond_to?(:new) ? auth.new(...) : auth.call(...) + end + + end + +end diff --git a/test/net/imap/test_imap_authenticators.rb b/test/net/imap/test_imap_authenticators.rb index 7e0c4fff..93c85817 100644 --- a/test/net/imap/test_imap_authenticators.rb +++ b/test/net/imap/test_imap_authenticators.rb @@ -5,11 +5,17 @@ class IMAPAuthenticatorsTest < Test::Unit::TestCase + def test_net_imap_authenticator_deprecated + assert_warn(/Net::IMAP\.authenticator .+deprecated./) do + Net::IMAP.authenticator("PLAIN", "user", "pass") + end + end + # ---------------------- # PLAIN # ---------------------- - def plain(...) Net::IMAP.authenticator("PLAIN", ...) end + def plain(...) Net::IMAP::SASL.authenticator("PLAIN", ...) end def test_plain_authenticator_matches_mechanism assert_kind_of(Net::IMAP::SASL::PlainAuthenticator, plain("user", "pass")) @@ -36,7 +42,7 @@ def test_plain_no_null_chars # XOAUTH2 # ---------------------- - def xoauth2(...) Net::IMAP.authenticator("XOAUTH2", ...) end + def xoauth2(...) Net::IMAP::SASL.authenticator("XOAUTH2", ...) end def test_xoauth2_authenticator_matches_mechanism assert_kind_of(Net::IMAP::SASL::XOAuth2Authenticator, xoauth2("user", "tok")) @@ -59,7 +65,7 @@ def test_xoauth2_supports_initial_response # ---------------------- def login(*args, warn_deprecation: false, **kwargs, &block) - Net::IMAP.authenticator( + Net::IMAP::SASL.authenticator( "LOGIN", *args, warn_deprecation: warn_deprecation, **kwargs, &block ) end @@ -74,7 +80,7 @@ def test_login_does_not_support_initial_response def test_login_authenticator_deprecated assert_warn(/LOGIN.+deprecated.+PLAIN/) do - Net::IMAP.authenticator("LOGIN", "user", "pass") + Net::IMAP::SASL.authenticator("LOGIN", "user", "pass") end end @@ -89,7 +95,7 @@ def test_login_responses # ---------------------- def cram_md5(*args, warn_deprecation: false, **kwargs, &block) - Net::IMAP.authenticator( + Net::IMAP::SASL.authenticator( "CRAM-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block ) end @@ -104,7 +110,7 @@ def test_cram_md5_does_not_support_initial_response def test_cram_md5_authenticator_deprecated assert_warn(/CRAM-MD5.+deprecated./) do - Net::IMAP.authenticator("CRAM-MD5", "user", "pass") + Net::IMAP::SASL.authenticator("CRAM-MD5", "user", "pass") end end @@ -119,7 +125,7 @@ def test_cram_md5_authenticator # ---------------------- def digest_md5(*args, warn_deprecation: false, **kwargs, &block) - Net::IMAP.authenticator( + Net::IMAP::SASL.authenticator( "DIGEST-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block ) end @@ -131,7 +137,7 @@ def test_digest_md5_authenticator_matches_mechanism def test_digest_md5_authenticator_deprecated assert_warn(/DIGEST-MD5.+deprecated.+RFC6331/) do - Net::IMAP.authenticator("DIGEST-MD5", "user", "pass") + Net::IMAP::SASL.authenticator("DIGEST-MD5", "user", "pass") end end