Skip to content

Define auth adapters #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Oct 13, 2015
Merged
8 changes: 8 additions & 0 deletions lib/net/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ 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'
require 'net/ldap/auth_adapter/gss_spnego'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we leave this adapter out of this PR since it's not one of the existing adapters we currently support? After this merges, we can create a separate gem and link to it from the README.


Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple)
Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl)
Net::LDAP::AuthAdapter.register(:gss_spnego, Net::LDAP::AuthAdapter::Sasl)

# == Quick-start for the Impatient
# === Quick Example of a user-authentication against an LDAP directory:
Expand Down
29 changes: 29 additions & 0 deletions lib/net/ldap/auth_adapter.rb
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions lib/net/ldap/auth_adapter/gss_spnego.rb
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions lib/net/ldap/auth_adapter/sasl.rb
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions lib/net/ldap/auth_adapter/simple.rb
Original file line number Diff line number Diff line change
@@ -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
123 changes: 2 additions & 121 deletions lib/net/ldap/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should test and handle the case when an adaptor is not found.

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
#
Expand Down
11 changes: 11 additions & 0 deletions test/test_auth_adapter.rb
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too bad that Connection.new actually tries to open the socket and forces us to stub here. Not suggesting any change, but making an observation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback. I had no idea so I referred to test/test_ldap_connection.rb
What would you do if you were me in this case?

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