Skip to content

Commit 0e1e416

Browse files
committed
🚚 Move SASL.authenticator registry from Net::IMAP
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.
1 parent d264b33 commit 0e1e416

File tree

5 files changed

+140
-68
lines changed

5 files changed

+140
-68
lines changed

lib/net/imap.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,10 +1074,7 @@ def starttls(options = {}, verify = true)
10741074
# completes. If the TaggedResponse to #authenticate includes updated
10751075
# capabilities, they will be cached.
10761076
def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback)
1077-
authenticator = self.class.authenticator(mechanism,
1078-
*creds,
1079-
**props,
1080-
&callback)
1077+
authenticator = SASL.authenticator(mechanism, *creds, **props, &callback)
10811078
cmdargs = ["AUTHENTICATE", mechanism]
10821079
if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) &&
10831080
SASL.initial_response?(authenticator)

lib/net/imap/authenticators.rb

Lines changed: 20 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,31 @@
11
# frozen_string_literal: true
22

3-
# Registry for SASL authenticators used by Net::IMAP.
3+
# Backward compatible delegators from Net::IMAP to Net::IMAP::SASL.
44
module Net::IMAP::Authenticators
55

6-
# Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
7-
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
8-
# implemented by +authenticator+ (for instance, <tt>"PLAIN"</tt>).
9-
#
10-
# The +authenticator+ must respond to +#new+ (or #call), receiving the
11-
# authenticator configuration and return a configured authentication session.
12-
# The authenticator session must respond to +#process+, receiving the server's
13-
# challenge and returning the client's response.
14-
#
15-
# See PlainAuthenticator, XOAuth2Authenticator, and DigestMD5Authenticator for
16-
# examples.
17-
def add_authenticator(auth_type, authenticator)
18-
authenticators[auth_type] = authenticator
6+
# Deprecated. Use Net::IMAP::SASL.add_authenticator instead.
7+
def add_authenticator(...)
8+
warn(
9+
"%s.%s is deprecated. Use %s.%s instead." % [
10+
Net::IMAP, __method__, Net::IMAP::SASL, __method__
11+
],
12+
uplevel: 1
13+
)
14+
Net::IMAP::SASL.add_authenticator(...)
1915
end
2016

21-
# :call-seq:
22-
# authenticator(mechanism, ...) -> authenticator
23-
# authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
24-
# authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator
25-
# authenticator(mechanism, **properties) -> authenticator
26-
# authenticator(mechanism) {|propname, authctx| value } -> authenticator
27-
#
28-
# Builds a new authentication session context for +mechanism+.
29-
#
30-
# [Note]
31-
# This method is intended for internal use by connection protocol code only.
32-
# Protocol client users should see refer to their client's documentation,
33-
# e.g. Net::IMAP#authenticate for Net::IMAP.
34-
#
35-
# The call signatures documented for this method are recommendations for
36-
# authenticator implementors. All arguments (other than +mechanism+) are
37-
# forwarded to the registered authenticator's +#new+ (or +#call+) method, and
38-
# each authenticator must document its own arguments.
39-
#
40-
# The returned object represents a single authentication exchange and <em>must
41-
# not</em> be reused for multiple authentication attempts.
42-
def authenticator(mechanism, ...)
43-
auth = authenticators.fetch(mechanism.upcase) do
44-
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
45-
end
46-
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
17+
# Deprecated. Use Net::IMAP::SASL.authenticator instead.
18+
def authenticator(...)
19+
warn(
20+
"%s.%s is deprecated. Use %s.%s instead." % [
21+
Net::IMAP, __method__, Net::IMAP::SASL, __method__
22+
],
23+
uplevel: 1
24+
)
25+
Net::IMAP::SASL.authenticator(...)
4726
end
4827

49-
private
50-
51-
def authenticators
52-
@authenticators ||= {}
53-
end
54-
55-
end
56-
57-
class Net::IMAP
58-
extend Authenticators
59-
add_authenticator "PLAIN", SASL::PlainAuthenticator
60-
add_authenticator "XOAUTH2", SASL::XOAuth2Authenticator
61-
62-
add_authenticator "CRAM-MD5", SASL::CramMD5Authenticator # deprecated
63-
add_authenticator "LOGIN", SASL::LoginAuthenticator # deprecated
64-
add_authenticator "DIGEST-MD5", SASL::DigestMD5Authenticator # deprecated
28+
Net::IMAP.extend self
6529
end
6630

6731
class Net::IMAP

lib/net/imap/sasl.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,26 @@ module SASL
3333
autoload :BidiStringError, sasl_stringprep_rb
3434

3535
sasl_dir = File.expand_path("sasl", __dir__)
36+
autoload :Authenticators, "#{sasl_dir}/authenticators"
37+
3638
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
3739
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
3840

3941
autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"
4042
autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator"
4143
autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"
4244

45+
# Returns the default global SASL::Authenticators instance.
46+
def self.authenticators
47+
@authenticators ||= Authenticators.new(use_defaults: true)
48+
end
49+
50+
# Delegates to ::authenticators. See Authenticators#authenticator.
51+
def self.authenticator(...) authenticators.authenticator(...) end
52+
53+
# Delegates to ::authenticators. See Authenticators#add_authenticator.
54+
def self.add_authenticator(...) authenticators.add_authenticator(...) end
55+
4356
module_function
4457

4558
# See Net::IMAP::StringPrep::SASLprep#saslprep.

lib/net/imap/sasl/authenticators.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
3+
module Net::IMAP::SASL
4+
5+
# Registry for SASL authenticators
6+
#
7+
# Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or
8+
# a proc), receiving any credentials and options and returning an
9+
# authenticator instance. The returned object represents a single
10+
# authentication exchange and <em>must not</em> be reused for multiple
11+
# authentication attempts.
12+
#
13+
# An authenticator instance object must respond to +#process+, receiving the
14+
# server's challenge and returning the client's response. Optionally, it may
15+
# also respond to +#initial_response?+ and +#done?+. When
16+
# +#initial_response?+ returns +true+, +#process+ may be called the first
17+
# time with +nil+. When +#done?+ returns +false+, the exchange is incomplete
18+
# and an exception should be raised if the exchange terminates prematurely.
19+
#
20+
# See the source for PlainAuthenticator, XOAuth2Authenticator, and
21+
# ScramSHA1Authenticator for examples.
22+
class Authenticators
23+
24+
# Create a new Authenticators registry.
25+
#
26+
# This class is usually not instantiated directly. Use SASL.authenticators
27+
# to reuse the default global registry.
28+
#
29+
# By default, the registry will be empty--without any registrations. When
30+
# +add_defaults+ is +true+, authenticators for all standard mechanisms will
31+
# be registered.
32+
#
33+
def initialize(use_defaults: false)
34+
@authenticators = {}
35+
if use_defaults
36+
add_authenticator "PLAIN", PlainAuthenticator
37+
add_authenticator "XOAUTH2", XOAuth2Authenticator
38+
add_authenticator "LOGIN", LoginAuthenticator # deprecated
39+
add_authenticator "CRAM-MD5", CramMD5Authenticator # deprecated
40+
add_authenticator "DIGEST-MD5", DigestMD5Authenticator # deprecated
41+
end
42+
end
43+
44+
# Returns the names of all registered SASL mechanisms.
45+
def names; @authenticators.keys end
46+
47+
# :call-seq:
48+
# add_authenticator(mechanism)
49+
# add_authenticator(mechanism, authenticator_class)
50+
# add_authenticator(mechanism, authenticator_proc)
51+
#
52+
# Registers an authenticator for #authenticator to use. +mechanism+ is the
53+
# name of the
54+
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
55+
# implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
56+
#
57+
# If +mechanism+ refers to an existing authenticator, a warning will be
58+
# printed and the old authenticator will be replaced.
59+
#
60+
# When only a single argument is given, the authenticator class will be
61+
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
62+
# preserved and non-alphanumeric characters are removed..
63+
def add_authenticator(auth_type, authenticator)
64+
@authenticators[auth_type] = authenticator
65+
end
66+
67+
# :call-seq:
68+
# authenticator(mechanism, ...) -> auth_session
69+
#
70+
# Builds an authenticator instance using the authenticator registered to
71+
# +mechanism+. The returned object represents a single authentication
72+
# exchange and <em>must not</em> be reused for multiple authentication
73+
# attempts.
74+
#
75+
# All arguments (except +mechanism+) are forwarded to the registered
76+
# authenticator's +#new+ or +#call+ method. Each authenticator must
77+
# document its own arguments.
78+
#
79+
# [Note]
80+
# This method is intended for internal use by connection protocol code
81+
# only. Protocol client users should see refer to their client's
82+
# documentation, e.g. Net::IMAP#authenticate.
83+
def authenticator(mechanism, ...)
84+
auth = @authenticators.fetch(mechanism.upcase) do
85+
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
86+
end
87+
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
88+
end
89+
90+
end
91+
92+
end

test/net/imap/test_imap_authenticators.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55

66
class IMAPAuthenticatorsTest < Test::Unit::TestCase
77

8+
def test_net_imap_authenticator_deprecated
9+
assert_warn(/Net::IMAP\.authenticator .+deprecated./) do
10+
Net::IMAP.authenticator("PLAIN", "user", "pass")
11+
end
12+
end
13+
814
# ----------------------
915
# PLAIN
1016
# ----------------------
1117

12-
def plain(...) Net::IMAP.authenticator("PLAIN", ...) end
18+
def plain(...) Net::IMAP::SASL.authenticator("PLAIN", ...) end
1319

1420
def test_plain_authenticator_matches_mechanism
1521
assert_kind_of(Net::IMAP::SASL::PlainAuthenticator, plain("user", "pass"))
@@ -36,7 +42,7 @@ def test_plain_no_null_chars
3642
# XOAUTH2
3743
# ----------------------
3844

39-
def xoauth2(...) Net::IMAP.authenticator("XOAUTH2", ...) end
45+
def xoauth2(...) Net::IMAP::SASL.authenticator("XOAUTH2", ...) end
4046

4147
def test_xoauth2_authenticator_matches_mechanism
4248
assert_kind_of(Net::IMAP::SASL::XOAuth2Authenticator, xoauth2("user", "tok"))
@@ -59,7 +65,7 @@ def test_xoauth2_supports_initial_response
5965
# ----------------------
6066

6167
def login(*args, warn_deprecation: false, **kwargs, &block)
62-
Net::IMAP.authenticator(
68+
Net::IMAP::SASL.authenticator(
6369
"LOGIN", *args, warn_deprecation: warn_deprecation, **kwargs, &block
6470
)
6571
end
@@ -74,7 +80,7 @@ def test_login_does_not_support_initial_response
7480

7581
def test_login_authenticator_deprecated
7682
assert_warn(/LOGIN.+deprecated.+PLAIN/) do
77-
Net::IMAP.authenticator("LOGIN", "user", "pass")
83+
Net::IMAP::SASL.authenticator("LOGIN", "user", "pass")
7884
end
7985
end
8086

@@ -89,7 +95,7 @@ def test_login_responses
8995
# ----------------------
9096

9197
def cram_md5(*args, warn_deprecation: false, **kwargs, &block)
92-
Net::IMAP.authenticator(
98+
Net::IMAP::SASL.authenticator(
9399
"CRAM-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block
94100
)
95101
end
@@ -104,7 +110,7 @@ def test_cram_md5_does_not_support_initial_response
104110

105111
def test_cram_md5_authenticator_deprecated
106112
assert_warn(/CRAM-MD5.+deprecated./) do
107-
Net::IMAP.authenticator("CRAM-MD5", "user", "pass")
113+
Net::IMAP::SASL.authenticator("CRAM-MD5", "user", "pass")
108114
end
109115
end
110116

@@ -119,7 +125,7 @@ def test_cram_md5_authenticator
119125
# ----------------------
120126

121127
def digest_md5(*args, warn_deprecation: false, **kwargs, &block)
122-
Net::IMAP.authenticator(
128+
Net::IMAP::SASL.authenticator(
123129
"DIGEST-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block
124130
)
125131
end
@@ -131,7 +137,7 @@ def test_digest_md5_authenticator_matches_mechanism
131137

132138
def test_digest_md5_authenticator_deprecated
133139
assert_warn(/DIGEST-MD5.+deprecated.+RFC6331/) do
134-
Net::IMAP.authenticator("DIGEST-MD5", "user", "pass")
140+
Net::IMAP::SASL.authenticator("DIGEST-MD5", "user", "pass")
135141
end
136142
end
137143

0 commit comments

Comments
 (0)