diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index ffb48719..7c151895 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -27,6 +27,12 @@ class LDAP require 'net/ldap/connection' require 'net/ldap/version' require 'net/ldap/error' +require 'net/ldap/auth_adapter' +require 'net/ldap/auth_adapter/simple' +require 'net/ldap/auth_adapter/sasl' + +Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple) +Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl) # == Quick-start for the Impatient # === Quick Example of a user-authentication against an LDAP directory: diff --git a/lib/net/ldap/auth_adapter.rb b/lib/net/ldap/auth_adapter.rb new file mode 100644 index 00000000..f74232d1 --- /dev/null +++ b/lib/net/ldap/auth_adapter.rb @@ -0,0 +1,29 @@ +module Net + class LDAP + class AuthAdapter + def self.register(names, adapter) + names = Array(names) + @adapters ||= {} + names.each do |name| + @adapters[name] = adapter + end + end + + def self.[](name) + a = @adapters[name] + if a.nil? + raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})" + end + return a + end + + def initialize(conn) + @connection = conn + end + + def bind + raise "bind method must be overwritten" + end + end + end +end diff --git a/lib/net/ldap/auth_adapter/gss_spnego.rb b/lib/net/ldap/auth_adapter/gss_spnego.rb new file mode 100644 index 00000000..e251f038 --- /dev/null +++ b/lib/net/ldap/auth_adapter/gss_spnego.rb @@ -0,0 +1,40 @@ +require 'net/ldap/auth_adapter' +require 'net/ldap/auth_adapter/sasl' + +module Net + class LDAP + module AuthAdapers + #-- + # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. + # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to + # integrate it without introducing an external dependency. + # + # This authentication method is accessed by calling #bind with a :method + # parameter of :gss_spnego. It requires :username and :password + # attributes, just like the :simple authentication method. It performs a + # GSS-SPNEGO authentication with the server, which is presumed to be a + # Microsoft Active Directory. + #++ + class GSS_SPNEGO < Net::LDAP::AuthAdapter + def bind(auth) + require 'ntlm' + + user, psw = [auth[:username] || auth[:dn], auth[:password]] + raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw) + + nego = proc { |challenge| + t2_msg = NTLM::Message.parse(challenge) + t3_msg = t2_msg.response({ :user => user, :password => psw }, + { :ntlmv2 => true }) + t3_msg.serialize + } + + Net::LDAP::AuthAdapter::Sasl.new(@connection). + bind(:method => :sasl, :mechanism => "GSS-SPNEGO", + :initial_credential => NTLM::Message::Type1.new.serialize, + :challenge_response => nego) + end + end + end + end +end diff --git a/lib/net/ldap/auth_adapter/sasl.rb b/lib/net/ldap/auth_adapter/sasl.rb new file mode 100644 index 00000000..fa7315b5 --- /dev/null +++ b/lib/net/ldap/auth_adapter/sasl.rb @@ -0,0 +1,60 @@ +require 'net/ldap/auth_adapter' + +module Net + class LDAP + class AuthAdapter + class Sasl < Net::LDAP::AuthAdapter + #-- + # Required parameters: :mechanism, :initial_credential and + # :challenge_response + # + # Mechanism is a string value that will be passed in the SASL-packet's + # "mechanism" field. + # + # Initial credential is most likely a string. It's passed in the initial + # BindRequest that goes to the server. In some protocols, it may be empty. + # + # Challenge-response is a Ruby proc that takes a single parameter and + # returns an object that will typically be a string. The + # challenge-response block is called when the server returns a + # BindResponse with a result code of 14 (saslBindInProgress). The + # challenge-response block receives a parameter containing the data + # returned by the server in the saslServerCreds field of the LDAP + # BindResponse packet. The challenge-response block may be called multiple + # times during the course of a SASL authentication, and each time it must + # return a value that will be passed back to the server as the credential + # data in the next BindRequest packet. + #++ + def bind(auth) + mech, cred, chall = auth[:mechanism], auth[:initial_credential], + auth[:challenge_response] + raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall) + + message_id = @connection.next_msgid + + n = 0 + loop { + sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) + request = [ + Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) + + @connection.send(:write, request, nil, message_id) + pdu = @connection.queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult + raise Net::LDAP::NoBindResultError, "no bind result" + end + + return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress + raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges) + + cred = chall.call(pdu.result_server_sasl_creds) + } + + raise Net::LDAP::SASLChallengeOverflowError, "why are we here?" + end + end + end + end +end diff --git a/lib/net/ldap/auth_adapter/simple.rb b/lib/net/ldap/auth_adapter/simple.rb new file mode 100644 index 00000000..d01b57ae --- /dev/null +++ b/lib/net/ldap/auth_adapter/simple.rb @@ -0,0 +1,34 @@ +require 'net/ldap/auth_adapter' + +module Net + class LDAP + class AuthAdapter + class Simple < AuthAdapter + def bind(auth) + user, psw = if auth[:method] == :simple + [auth[:username] || auth[:dn], auth[:password]] + else + ["", ""] + end + + raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw) + + message_id = @connection.next_msgid + request = [ + Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber, + psw.to_ber_contextspecific(0) + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) + + @connection.send(:write, request, nil, message_id) + pdu = @connection.queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult + raise Net::LDAP::NoBindResultError, "no bind result" + end + + pdu + end + end + end + end +end diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index 05aedfef..f45e54a0 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -250,130 +250,11 @@ def next_msgid def bind(auth) instrument "bind.net_ldap_connection" do |payload| payload[:method] = meth = auth[:method] - if [:simple, :anonymous, :anon].include?(meth) - bind_simple auth - elsif meth == :sasl - bind_sasl(auth) - elsif meth == :gss_spnego - bind_gss_spnego(auth) - else - raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{meth})" - end - end - end - - #-- - # Implements a simple user/psw authentication. Accessed by calling #bind - # with a method of :simple or :anonymous. - #++ - def bind_simple(auth) - user, psw = if auth[:method] == :simple - [auth[:username] || auth[:dn], auth[:password]] - else - ["", ""] - end - - raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw) - - message_id = next_msgid - request = [ - LdapVersion.to_ber, user.to_ber, - psw.to_ber_contextspecific(0) - ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) - - write(request, nil, message_id) - pdu = queued_read(message_id) - - if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult - raise Net::LDAP::NoBindResultError, "no bind result" + adapter = Net::LDAP::AuthAdapter[meth] + adapter.new(self).bind(auth) end - - pdu end - #-- - # Required parameters: :mechanism, :initial_credential and - # :challenge_response - # - # Mechanism is a string value that will be passed in the SASL-packet's - # "mechanism" field. - # - # Initial credential is most likely a string. It's passed in the initial - # BindRequest that goes to the server. In some protocols, it may be empty. - # - # Challenge-response is a Ruby proc that takes a single parameter and - # returns an object that will typically be a string. The - # challenge-response block is called when the server returns a - # BindResponse with a result code of 14 (saslBindInProgress). The - # challenge-response block receives a parameter containing the data - # returned by the server in the saslServerCreds field of the LDAP - # BindResponse packet. The challenge-response block may be called multiple - # times during the course of a SASL authentication, and each time it must - # return a value that will be passed back to the server as the credential - # data in the next BindRequest packet. - #++ - def bind_sasl(auth) - mech, cred, chall = auth[:mechanism], auth[:initial_credential], - auth[:challenge_response] - raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall) - - message_id = next_msgid - - n = 0 - loop { - sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) - request = [ - LdapVersion.to_ber, "".to_ber, sasl - ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) - - write(request, nil, message_id) - pdu = queued_read(message_id) - - if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult - raise Net::LDAP::NoBindResultError, "no bind result" - end - - return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress - raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges) - - cred = chall.call(pdu.result_server_sasl_creds) - } - - raise Net::LDAP::SASLChallengeOverflowError, "why are we here?" - end - private :bind_sasl - - #-- - # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. - # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to - # integrate it without introducing an external dependency. - # - # This authentication method is accessed by calling #bind with a :method - # parameter of :gss_spnego. It requires :username and :password - # attributes, just like the :simple authentication method. It performs a - # GSS-SPNEGO authentication with the server, which is presumed to be a - # Microsoft Active Directory. - #++ - def bind_gss_spnego(auth) - require 'ntlm' - - user, psw = [auth[:username] || auth[:dn], auth[:password]] - raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw) - - nego = proc { |challenge| - t2_msg = NTLM::Message.parse(challenge) - t3_msg = t2_msg.response({ :user => user, :password => psw }, - { :ntlmv2 => true }) - t3_msg.serialize - } - - bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO", - :initial_credential => NTLM::Message::Type1.new.serialize, - :challenge_response => nego) - end - private :bind_gss_spnego - - #-- # Allow the caller to specify a sort control # diff --git a/test/test_auth_adapter.rb b/test/test_auth_adapter.rb new file mode 100644 index 00000000..7cec57bc --- /dev/null +++ b/test/test_auth_adapter.rb @@ -0,0 +1,11 @@ +require 'test_helper' + +class TestAuthAdapter < Test::Unit::TestCase + def test_undefined_auth_adapter + flexmock(TCPSocket).should_receive(:new).ordered.with('ldap.example.com', 379).once.and_return(nil) + conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379) + assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do + conn.bind(method: :foo) + end + end +end