Skip to content

Commit 057feb1

Browse files
committed
🚚 Move SASL.authenticator registry from Net::IMAP
Also change `Net::IMAP::SASL.add_authenticator` to be case insensitive for the mechanism name. 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 ad11f99 commit 057feb1

File tree

10 files changed

+123
-71
lines changed

10 files changed

+123
-71
lines changed

lib/net/imap.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,10 +1049,7 @@ def starttls(options = {}, verify = true)
10491049
# completes. If the TaggedResponse to #authenticate includes updated
10501050
# capabilities, they will be cached.
10511051
def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback)
1052-
authenticator = self.class.authenticator(mechanism,
1053-
*creds,
1054-
**props,
1055-
&callback)
1052+
authenticator = SASL.authenticator(mechanism, *creds, **props, &callback)
10561053
cmdargs = ["AUTHENTICATE", mechanism]
10571054
if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) &&
10581055
SASL.initial_response?(authenticator)

lib/net/imap/authenticators.rb

Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,30 @@
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(...)
47-
end
48-
49-
private
50-
51-
def authenticators
52-
@authenticators ||= {}
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(...)
5326
end
5427

5528
end
5629

5730
Net::IMAP.extend Net::IMAP::Authenticators
58-
59-
require_relative "authenticators/plain"
60-
61-
require_relative "authenticators/login"
62-
require_relative "authenticators/cram_md5"
63-
require_relative "authenticators/digest_md5"
64-
require_relative "authenticators/xoauth2"

lib/net/imap/authenticators/cram_md5.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,4 @@ def hmac_md5(text, key)
4747
return Digest::MD5.hexdigest(k_opad + digest)
4848
end
4949

50-
Net::IMAP.add_authenticator "CRAM-MD5", self
5150
end

lib/net/imap/authenticators/digest_md5.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,4 @@ def qdval(k, v)
111111
end
112112
end
113113

114-
Net::IMAP.add_authenticator "DIGEST-MD5", self
115114
end

lib/net/imap/authenticators/login.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,4 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
4242
@state = STATE_USER
4343
end
4444

45-
Net::IMAP.add_authenticator "LOGIN", self
4645
end

lib/net/imap/authenticators/plain.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,4 @@ def initialize(username, password, authzid: nil)
3939
@authzid = authzid
4040
end
4141

42-
Net::IMAP.add_authenticator "PLAIN", self
4342
end

lib/net/imap/authenticators/xoauth2.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,4 @@ def build_oauth2_string(user, oauth2_token)
1919
format("user=%s\1auth=Bearer %s\1\1", user, oauth2_token)
2020
end
2121

22-
Net::IMAP.add_authenticator 'XOAUTH2', self
2322
end

lib/net/imap/sasl.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,25 @@ module SASL
5959
autoload :ProhibitedCodepoint, stringprep_path
6060
autoload :BidiStringError, stringprep_path
6161

62+
sasl_dir = File.expand_path("sasl", __dir__)
63+
autoload :Authenticators, "#{sasl_dir}/authenticators"
64+
65+
# Authenticators are all lazy loaded
66+
def self.authenticators
67+
@authenticators ||= SASL::Authenticators.new.tap do |registry|
68+
registry.add_authenticator "Plain", PlainAuthenticator
69+
registry.add_authenticator "XOauth2", XOauth2Authenticator
70+
registry.add_authenticator "Login", LoginAuthenticator # deprecated
71+
registry.add_authenticator "Cram-MD5", CramMD5Authenticator # deprecated
72+
registry.add_authenticator "Digest-MD5", DigestMD5Authenticator # deprecated
73+
end
74+
end
75+
76+
# Delegates to authenticators. See Authenticators#authenticator.
77+
def self.authenticator(...) authenticators.authenticator(...) end
78+
79+
# See SASL::add_authenticator
80+
def self.add_authenticator(...) authenticators.add_authenticator(...) end
6281

6382
module_function
6483

@@ -73,5 +92,10 @@ def initial_response?(mechanism)
7392

7493
end
7594
end
76-
7795
end
96+
97+
require_relative "authenticators/plain"
98+
require_relative "authenticators/login"
99+
require_relative "authenticators/cram_md5"
100+
require_relative "authenticators/digest_md5"
101+
require_relative "authenticators/xoauth2"

lib/net/imap/sasl/authenticators.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
module Net::IMAP::SASL
4+
5+
# Registry for SASL mechanism authenticators
6+
class Authenticators
7+
8+
def initialize
9+
@authenticators = {}
10+
end
11+
12+
# Returns the hash of SASL mechanism names to registered authenticators.
13+
def authenticators; @authenticators.dup end
14+
15+
# :call-seq:
16+
# add_authenticator(sasl_mechanism_name, authenticator_class)
17+
# add_authenticator(sasl_mechanism_name, authenticator_proc)
18+
#
19+
# Adds an authenticator for #authenticator to use. +mechanism+ is the
20+
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
21+
# implemented by +authenticator+ (for instance, <tt>"PLAIN"</tt>).
22+
#
23+
# The +authenticator+ must respond to +#new+ (or #call), receiving the
24+
# authenticator configuration and return a configured authentication session.
25+
# The authenticator session must respond to +#process+, receiving the server's
26+
# challenge and returning the client's response.
27+
#
28+
# See PlainAuthenticator, XOauth2Authenticator, and DigestMD5Authenticator for
29+
# examples.
30+
def add_authenticator(mechanism, authenticator)
31+
@authenticators[mechanism.upcase] = authenticator
32+
end
33+
34+
# :call-seq:
35+
# authenticator(mechanism, ...) -> authenticator
36+
# authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
37+
# authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator
38+
# authenticator(mechanism, **properties) -> authenticator
39+
# authenticator(mechanism) {|propname, authctx| value } -> authenticator
40+
#
41+
# Builds a new authentication session context for +mechanism+.
42+
#
43+
# [Note]
44+
# This method is intended for internal use by connection protocol code only.
45+
# Protocol client users should see refer to their client's documentation,
46+
# e.g. Net::IMAP#authenticate.
47+
#
48+
# The call signatures documented for this method are generic recommendations
49+
# for authenticators. All arguments (other than +mechanism+) are forwarded to
50+
# the registered authenticator's +#new+ (or +#call+) method, and each
51+
# authenticator must document its own arguments.
52+
#
53+
# The returned object represents a single authentication exchange and <em>must
54+
# not</em> be reused for multiple authentication attempts.
55+
def authenticator(mechanism, ...)
56+
auth = @authenticators.fetch(mechanism.upcase) do
57+
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
58+
end
59+
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
60+
end
61+
62+
end
63+
64+
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::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::XOauth2Authenticator, xoauth2("user", "pass"))
@@ -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
@@ -130,7 +136,7 @@ def test_digest_md5_authenticator_matches_mechanism
130136

131137
def test_digest_md5_authenticator_deprecated
132138
assert_warn(/DIGEST-MD5.+deprecated.+RFC6331/) do
133-
Net::IMAP.authenticator("DIGEST-MD5", "user", "pass")
139+
Net::IMAP::SASL.authenticator("DIGEST-MD5", "user", "pass")
134140
end
135141
end
136142

0 commit comments

Comments
 (0)