Skip to content

Commit 1429d03

Browse files
author
Luiz Angelo Daros de Luca
committed
- added support for SASL authentication with gem ruby-sasl. This allows
DIGEST-MD5, GSS-SPNEGO, GSSAPI authentication. - unified @conn.bind call to conn_bind method - appended :host information for bind auth args as it is necessary for some SASL mechs - added support for SASL security layer (similar to SSL). Currently only for gssapi/kerberos, but changing only ruby-sasl, it might be easy to implement.
1 parent 95ba94f commit 1429d03

File tree

1 file changed

+74
-40
lines changed

1 file changed

+74
-40
lines changed

lib/net/ldap.rb

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ class LDAP
1313
HasOpenSSL = false
1414
# :startdoc:
1515
end
16+
begin
17+
require 'sasl'
18+
##
19+
# Set to +true+ if SASL is available
20+
HasSASL = true
21+
rescue LoadError
22+
# :stopdoc:
23+
HasSASL = false
24+
# :startdoc:
25+
end
1626
end
1727
end
1828
require 'socket'
@@ -576,7 +586,8 @@ def open
576586
:port => @port,
577587
:encryption =>
578588
@encryption)
579-
@open_connection.bind(@auth)
589+
590+
conn_bind(@open_connection,@auth)
580591
yield self
581592
ensure
582593
@open_connection.close if @open_connection
@@ -650,7 +661,7 @@ def search(args = {})
650661
begin
651662
conn = Net::LDAP::Connection.new(:host => @host, :port => @port,
652663
:encryption => @encryption)
653-
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
664+
if (@result = conn_bind(conn,args[:auth] || @auth)).result_code == 0
654665
@result = conn.search(args) { |entry|
655666
result_set << entry if result_set
656667
yield entry if block_given?
@@ -727,12 +738,12 @@ def search(args = {})
727738
# instead of a String.
728739
def bind(auth = @auth)
729740
if @open_connection
730-
@result = @open_connection.bind(auth)
741+
@result = conn_bind(@open_connection,auth)
731742
else
732743
begin
733744
conn = Connection.new(:host => @host, :port => @port,
734745
:encryption => @encryption)
735-
@result = conn.bind(auth)
746+
@result = conn_bind(conn,auth)
736747
ensure
737748
conn.close if conn
738749
end
@@ -741,6 +752,13 @@ def bind(auth = @auth)
741752
@result.success?
742753
end
743754

755+
def conn_bind(conn, auth)
756+
auth = auth.dup
757+
auth[:host] = @host unless auth.include?(:host)
758+
conn.bind(auth)
759+
end
760+
private :conn_bind
761+
744762
# #bind_as is for testing authentication credentials.
745763
#
746764
# As described under #bind, most LDAP servers require that you supply a
@@ -833,7 +851,7 @@ def add(args)
833851
begin
834852
conn = Connection.new(:host => @host, :port => @port,
835853
:encryption => @encryption)
836-
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
854+
if (@result = conn_bind(conn, args[:auth] || @auth)).result_code == 0
837855
@result = conn.add(args)
838856
end
839857
ensure
@@ -931,7 +949,7 @@ def modify(args)
931949
begin
932950
conn = Connection.new(:host => @host, :port => @port,
933951
:encryption => @encryption)
934-
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
952+
if (@result = conn_bind(conn,args[:auth] || @auth)).result_code == 0
935953
@result = conn.modify(args)
936954
end
937955
ensure
@@ -1003,7 +1021,7 @@ def rename(args)
10031021
begin
10041022
conn = Connection.new(:host => @host, :port => @port,
10051023
:encryption => @encryption)
1006-
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
1024+
if (@result = conn_bind(conn,args[:auth] || @auth)).result_code == 0
10071025
@result = conn.rename(args)
10081026
end
10091027
ensure
@@ -1031,7 +1049,7 @@ def delete(args)
10311049
begin
10321050
conn = Connection.new(:host => @host, :port => @port,
10331051
:encryption => @encryption)
1034-
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
1052+
if (@result = conn_bind(conn,args[:auth] || @auth)).result_code == 0
10351053
@result = conn.delete(args)
10361054
end
10371055
ensure
@@ -1172,6 +1190,14 @@ def self.wrap_with_ssl(io)
11721190
conn
11731191
end
11741192

1193+
def self.wrap_with_sasl(io, securelayer_wrapper)
1194+
raise Net::LDAP::LdapError, "SASL library (gem ruby-sasl) is unavailable" unless Net::LDAP::HasSASL
1195+
conn = securelayer_wrapper.call(io)
1196+
conn.extend(Net::BER::BERParser)
1197+
conn.extend(SASL::Buffering)
1198+
conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
1199+
end
1200+
11751201
#--
11761202
# Helper method called only from new, and only after we have a
11771203
# successfully-opened @conn instance variable, which is a TCP connection.
@@ -1276,14 +1302,22 @@ def bind_simple(auth)
12761302
end
12771303

12781304
#--
1279-
# Required parameters: :mechanism, :initial_credential and
1280-
# :challenge_response
1305+
# Required parameters: :mechanism
12811306
#
1282-
# Mechanism is a string value that will be passed in the SASL-packet's
1283-
# "mechanism" field.
1307+
# If ruby-sasl is avaiable, :host is used to setup digest-url and defaults to
1308+
# connection hostname. Also, for some mechs, the :username and :password are used.
1309+
# :initial_credential and :challenge_response are optional and filled by
1310+
# setup_auth_sasl.
1311+
#
1312+
# If ruby-sasl is not avaiable, :initial_credential and :challenge_response
1313+
# are required.
12841314
#
12851315
# Initial credential is most likely a string. It's passed in the initial
1286-
# BindRequest that goes to the server. In some protocols, it may be empty.
1316+
# BindRequest that goes to the server. In some protocols, it may be empty
1317+
# or missing.
1318+
#
1319+
# Mechanism is a string value that will be passed in the SASL-packet's
1320+
# "mechanism" field.
12871321
#
12881322
# Challenge-response is a Ruby proc that takes a single parameter and
12891323
# returns an object that will typically be a string. The
@@ -1295,13 +1329,18 @@ def bind_simple(auth)
12951329
# times during the course of a SASL authentication, and each time it must
12961330
# return a value that will be passed back to the server as the credential
12971331
# data in the next BindRequest packet.
1332+
#
12981333
#++
12991334
def bind_sasl(auth)
1335+
if not auth[:challenge_response]
1336+
auth = auth.merge(self.class.setup_auth_sasl(auth))
1337+
end
13001338
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
13011339
auth[:challenge_response]
13021340
raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && chall)
13031341

13041342
n = 0
1343+
securelayer_wrapper = nil
13051344
loop {
13061345
msgid = next_msgid.to_ber
13071346
if cred
@@ -1314,46 +1353,41 @@ def bind_sasl(auth)
13141353
@conn.write request_pkt
13151354

13161355
(be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1356+
if securelayer_wrapper
1357+
$stderr.puts "secure lay"
1358+
@conn = self.class.wrap_with_sasl(@conn, securelayer_wrapper)
1359+
end
13171360
return pdu unless pdu.result_code == 14 # saslBindInProgress
13181361
raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
13191362

1320-
cred = chall.call(pdu.result_server_sasl_creds)
1363+
(cred, securelayer_wrapper) = chall.call(pdu.result_server_sasl_creds)
13211364
}
13221365

13231366
raise Net::LDAP::LdapError, "why are we here?"
13241367
end
13251368
private :bind_sasl
13261369

1327-
#--
1328-
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
1329-
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
1330-
# integrate it without introducing an external dependency.
1331-
#
1332-
# This authentication method is accessed by calling #bind with a :method
1333-
# parameter of :gss_spnego. It requires :username and :password
1334-
# attributes, just like the :simple authentication method. It performs a
1335-
# GSS-SPNEGO authentication with the server, which is presumed to be a
1336-
# Microsoft Active Directory.
1337-
#++
1338-
def bind_gss_spnego(auth)
1339-
require 'ntlm'
1370+
SASL_SERVICE_NAME="ldap"
13401371

1341-
user, psw = [auth[:username] || auth[:dn], auth[:password]]
1342-
raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1372+
def self.setup_auth_sasl(auth)
1373+
raise Net::LDAP::LdapError, "SASL library (gem ruby-sasl) is unavailable" unless Net::LDAP::HasSASL
1374+
raise Net::LDAP::LdapError, "Invalid binding information. auth[:host] is required" unless auth.include?(:host)
13431375

1344-
nego = proc { |challenge|
1345-
t2_msg = NTLM::Message.parse(challenge)
1346-
t3_msg = t2_msg.response({ :user => user, :password => psw },
1347-
{ :ntlmv2 => true })
1348-
t3_msg.serialize
1349-
}
1376+
pref = SASL::Preferences.new(
1377+
:digest_uri =>"#{SASL_SERVICE_NAME}/#{auth[:host]}",
1378+
:username => auth[:username] || auth[:dn],
1379+
:password => auth[:password],
1380+
)
1381+
sasl = SASL.new_mechanism(auth[:mechanism], pref)
13501382

1351-
bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
1352-
:initial_credential => NTLM::Message::Type1.new.serialize,
1353-
:challenge_response => nego)
1354-
end
1355-
private :bind_gss_spnego
1383+
challenge_response = Proc.new do |cred|
1384+
response = sasl.receive("challenge", cred)
1385+
response[1]
1386+
end
13561387

1388+
initial_credential = sasl.start[1]
1389+
return {:initial_credential => initial_credential, :challenge_response=>challenge_response}
1390+
end
13571391

13581392
#--
13591393
# Allow the caller to specify a sort control

0 commit comments

Comments
 (0)