Skip to content

Commit 4667a08

Browse files
committed
Merge pull request #226 from satoryu/define_auth_adapters
Define auth adapters
2 parents 4157684 + 8be5224 commit 4667a08

File tree

7 files changed

+182
-121
lines changed

7 files changed

+182
-121
lines changed

lib/net/ldap.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ class LDAP
2727
require 'net/ldap/connection'
2828
require 'net/ldap/version'
2929
require 'net/ldap/error'
30+
require 'net/ldap/auth_adapter'
31+
require 'net/ldap/auth_adapter/simple'
32+
require 'net/ldap/auth_adapter/sasl'
33+
34+
Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple)
35+
Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl)
3036

3137
# == Quick-start for the Impatient
3238
# === Quick Example of a user-authentication against an LDAP directory:

lib/net/ldap/auth_adapter.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module Net
2+
class LDAP
3+
class AuthAdapter
4+
def self.register(names, adapter)
5+
names = Array(names)
6+
@adapters ||= {}
7+
names.each do |name|
8+
@adapters[name] = adapter
9+
end
10+
end
11+
12+
def self.[](name)
13+
a = @adapters[name]
14+
if a.nil?
15+
raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})"
16+
end
17+
return a
18+
end
19+
20+
def initialize(conn)
21+
@connection = conn
22+
end
23+
24+
def bind
25+
raise "bind method must be overwritten"
26+
end
27+
end
28+
end
29+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
require 'net/ldap/auth_adapter'
2+
require 'net/ldap/auth_adapter/sasl'
3+
4+
module Net
5+
class LDAP
6+
module AuthAdapers
7+
#--
8+
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
9+
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
10+
# integrate it without introducing an external dependency.
11+
#
12+
# This authentication method is accessed by calling #bind with a :method
13+
# parameter of :gss_spnego. It requires :username and :password
14+
# attributes, just like the :simple authentication method. It performs a
15+
# GSS-SPNEGO authentication with the server, which is presumed to be a
16+
# Microsoft Active Directory.
17+
#++
18+
class GSS_SPNEGO < Net::LDAP::AuthAdapter
19+
def bind(auth)
20+
require 'ntlm'
21+
22+
user, psw = [auth[:username] || auth[:dn], auth[:password]]
23+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
24+
25+
nego = proc { |challenge|
26+
t2_msg = NTLM::Message.parse(challenge)
27+
t3_msg = t2_msg.response({ :user => user, :password => psw },
28+
{ :ntlmv2 => true })
29+
t3_msg.serialize
30+
}
31+
32+
Net::LDAP::AuthAdapter::Sasl.new(@connection).
33+
bind(:method => :sasl, :mechanism => "GSS-SPNEGO",
34+
:initial_credential => NTLM::Message::Type1.new.serialize,
35+
:challenge_response => nego)
36+
end
37+
end
38+
end
39+
end
40+
end

lib/net/ldap/auth_adapter/sasl.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
require 'net/ldap/auth_adapter'
2+
3+
module Net
4+
class LDAP
5+
class AuthAdapter
6+
class Sasl < Net::LDAP::AuthAdapter
7+
#--
8+
# Required parameters: :mechanism, :initial_credential and
9+
# :challenge_response
10+
#
11+
# Mechanism is a string value that will be passed in the SASL-packet's
12+
# "mechanism" field.
13+
#
14+
# Initial credential is most likely a string. It's passed in the initial
15+
# BindRequest that goes to the server. In some protocols, it may be empty.
16+
#
17+
# Challenge-response is a Ruby proc that takes a single parameter and
18+
# returns an object that will typically be a string. The
19+
# challenge-response block is called when the server returns a
20+
# BindResponse with a result code of 14 (saslBindInProgress). The
21+
# challenge-response block receives a parameter containing the data
22+
# returned by the server in the saslServerCreds field of the LDAP
23+
# BindResponse packet. The challenge-response block may be called multiple
24+
# times during the course of a SASL authentication, and each time it must
25+
# return a value that will be passed back to the server as the credential
26+
# data in the next BindRequest packet.
27+
#++
28+
def bind(auth)
29+
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
30+
auth[:challenge_response]
31+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
32+
33+
message_id = @connection.next_msgid
34+
35+
n = 0
36+
loop {
37+
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
38+
request = [
39+
Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl
40+
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
41+
42+
@connection.send(:write, request, nil, message_id)
43+
pdu = @connection.queued_read(message_id)
44+
45+
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
46+
raise Net::LDAP::NoBindResultError, "no bind result"
47+
end
48+
49+
return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
50+
raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
51+
52+
cred = chall.call(pdu.result_server_sasl_creds)
53+
}
54+
55+
raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
56+
end
57+
end
58+
end
59+
end
60+
end

lib/net/ldap/auth_adapter/simple.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
require 'net/ldap/auth_adapter'
2+
3+
module Net
4+
class LDAP
5+
class AuthAdapter
6+
class Simple < AuthAdapter
7+
def bind(auth)
8+
user, psw = if auth[:method] == :simple
9+
[auth[:username] || auth[:dn], auth[:password]]
10+
else
11+
["", ""]
12+
end
13+
14+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
15+
16+
message_id = @connection.next_msgid
17+
request = [
18+
Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber,
19+
psw.to_ber_contextspecific(0)
20+
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
21+
22+
@connection.send(:write, request, nil, message_id)
23+
pdu = @connection.queued_read(message_id)
24+
25+
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
26+
raise Net::LDAP::NoBindResultError, "no bind result"
27+
end
28+
29+
pdu
30+
end
31+
end
32+
end
33+
end
34+
end

lib/net/ldap/connection.rb

Lines changed: 2 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -250,130 +250,11 @@ def next_msgid
250250
def bind(auth)
251251
instrument "bind.net_ldap_connection" do |payload|
252252
payload[:method] = meth = auth[:method]
253-
if [:simple, :anonymous, :anon].include?(meth)
254-
bind_simple auth
255-
elsif meth == :sasl
256-
bind_sasl(auth)
257-
elsif meth == :gss_spnego
258-
bind_gss_spnego(auth)
259-
else
260-
raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{meth})"
261-
end
262-
end
263-
end
264-
265-
#--
266-
# Implements a simple user/psw authentication. Accessed by calling #bind
267-
# with a method of :simple or :anonymous.
268-
#++
269-
def bind_simple(auth)
270-
user, psw = if auth[:method] == :simple
271-
[auth[:username] || auth[:dn], auth[:password]]
272-
else
273-
["", ""]
274-
end
275-
276-
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
277-
278-
message_id = next_msgid
279-
request = [
280-
LdapVersion.to_ber, user.to_ber,
281-
psw.to_ber_contextspecific(0)
282-
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
283-
284-
write(request, nil, message_id)
285-
pdu = queued_read(message_id)
286-
287-
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
288-
raise Net::LDAP::NoBindResultError, "no bind result"
253+
adapter = Net::LDAP::AuthAdapter[meth]
254+
adapter.new(self).bind(auth)
289255
end
290-
291-
pdu
292256
end
293257

294-
#--
295-
# Required parameters: :mechanism, :initial_credential and
296-
# :challenge_response
297-
#
298-
# Mechanism is a string value that will be passed in the SASL-packet's
299-
# "mechanism" field.
300-
#
301-
# Initial credential is most likely a string. It's passed in the initial
302-
# BindRequest that goes to the server. In some protocols, it may be empty.
303-
#
304-
# Challenge-response is a Ruby proc that takes a single parameter and
305-
# returns an object that will typically be a string. The
306-
# challenge-response block is called when the server returns a
307-
# BindResponse with a result code of 14 (saslBindInProgress). The
308-
# challenge-response block receives a parameter containing the data
309-
# returned by the server in the saslServerCreds field of the LDAP
310-
# BindResponse packet. The challenge-response block may be called multiple
311-
# times during the course of a SASL authentication, and each time it must
312-
# return a value that will be passed back to the server as the credential
313-
# data in the next BindRequest packet.
314-
#++
315-
def bind_sasl(auth)
316-
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
317-
auth[:challenge_response]
318-
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
319-
320-
message_id = next_msgid
321-
322-
n = 0
323-
loop {
324-
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
325-
request = [
326-
LdapVersion.to_ber, "".to_ber, sasl
327-
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
328-
329-
write(request, nil, message_id)
330-
pdu = queued_read(message_id)
331-
332-
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
333-
raise Net::LDAP::NoBindResultError, "no bind result"
334-
end
335-
336-
return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
337-
raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
338-
339-
cred = chall.call(pdu.result_server_sasl_creds)
340-
}
341-
342-
raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
343-
end
344-
private :bind_sasl
345-
346-
#--
347-
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
348-
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
349-
# integrate it without introducing an external dependency.
350-
#
351-
# This authentication method is accessed by calling #bind with a :method
352-
# parameter of :gss_spnego. It requires :username and :password
353-
# attributes, just like the :simple authentication method. It performs a
354-
# GSS-SPNEGO authentication with the server, which is presumed to be a
355-
# Microsoft Active Directory.
356-
#++
357-
def bind_gss_spnego(auth)
358-
require 'ntlm'
359-
360-
user, psw = [auth[:username] || auth[:dn], auth[:password]]
361-
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
362-
363-
nego = proc { |challenge|
364-
t2_msg = NTLM::Message.parse(challenge)
365-
t3_msg = t2_msg.response({ :user => user, :password => psw },
366-
{ :ntlmv2 => true })
367-
t3_msg.serialize
368-
}
369-
370-
bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
371-
:initial_credential => NTLM::Message::Type1.new.serialize,
372-
:challenge_response => nego)
373-
end
374-
private :bind_gss_spnego
375-
376-
377258
#--
378259
# Allow the caller to specify a sort control
379260
#

test/test_auth_adapter.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require 'test_helper'
2+
3+
class TestAuthAdapter < Test::Unit::TestCase
4+
def test_undefined_auth_adapter
5+
flexmock(TCPSocket).should_receive(:new).ordered.with('ldap.example.com', 379).once.and_return(nil)
6+
conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379)
7+
assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do
8+
conn.bind(method: :foo)
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)