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
25 changes: 25 additions & 0 deletions lib/net/ldap/auth_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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)
@adapters[name]
end

def initialize(conn)
@connection = conn
end

def bind
raise "bind method must be overwritten"
end
end
end
end
3 changes: 3 additions & 0 deletions lib/net/ldap/auth_adapters/anon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require 'net/ldap/auth_adapters/simple'

Net::LDAP::AuthAdapter.register(:anon, Net::LDAP::AuthAdapters::Simple)
3 changes: 3 additions & 0 deletions lib/net/ldap/auth_adapters/anonymous.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require 'net/ldap/auth_adapters/simple'

Net::LDAP::AuthAdapter.register(:anonymous, Net::LDAP::AuthAdapters::Simple)
Copy link
Member

Choose a reason for hiding this comment

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

Why have both :anon and :anonymous? Is this for backwards compatibility?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see the previous case statement works with :simple, :anonymous, :anon

42 changes: 42 additions & 0 deletions lib/net/ldap/auth_adapters/gss_spnego.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'net/ldap/auth_adapter'
require 'net/ldap/auth_adapters/sasl'
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need sasl here?


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'
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This adapter depends on ntlm as the above comments point out. now this can be defined as a gem like net-ldap-auth_adapters-gss_spnego.


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.new(@connection).
bind(:method => :sasl, :mechanism => "GSS-SPNEGO",
:initial_credential => NTLM::Message::Type1.new.serialize,
:challenge_response => nego)
end
end
end
end
end

Net::LDAP::Adapter.register(:gss_spnego, Net::LDAP::AuthAdapters::GSS_SPNEGO)
62 changes: 62 additions & 0 deletions lib/net/ldap/auth_adapters/sasl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'net/ldap/auth_adapter'

module Net
class LDAP
module AuthAdapters
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

Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapters::Sasl)
36 changes: 36 additions & 0 deletions lib/net/ldap/auth_adapters/simple.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'net/ldap/auth_adapter'

module Net
class LDAP
module AuthAdapters
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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Calling a private method looks dirty... do you have any idea?

Copy link
Member

Choose a reason for hiding this comment

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

I agree, but this is the existing implementation, so let's change this in a separate PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I got. thanks.

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

Net::LDAP::AuthAdapter.register(:simple, Net::LDAP::AuthAdapters::Simple)
124 changes: 3 additions & 121 deletions lib/net/ldap/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,130 +250,12 @@ 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"
require "net/ldap/auth_adapters/#{meth}"
Copy link
Member

Choose a reason for hiding this comment

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

This works, but I prefer to have requires at the top of a file to make it obvious. Also, until we extract the other adaptors into gems, I'd suggest putting these requires in lib/net/ldap.rb:

require 'net/ldap/auth_adapter'
require 'net/ldap/auth_adapter/simple'  # <-- side note, can we rename the folder and namespace to be singular?
require 'net/ldap/auth_adapter/sasl'
# ...

# Move all of the registration to a single place, decouples the definition of the adapter from it's registration and use
Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapters::Simple)
Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapters::Sasl)

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