Skip to content

Commit 4ab7645

Browse files
author
Rory OConnell
committed
Merge
2 parents 3345c58 + ad4493b commit 4ab7645

File tree

10 files changed

+191
-47
lines changed

10 files changed

+191
-47
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ publish/
99
coverage/
1010
coverage.info
1111
.rake_tasks~
12+
Gemfile.lock

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source :rubygems
2+
gemspec

lib/net/ber/core_ext/array.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,18 @@ def to_ber_oid
7979
oid = ary.pack("w*")
8080
[6, oid.length].pack("CC") + oid
8181
end
82+
83+
##
84+
# Converts an array into a set of ber control codes
85+
# The expected format is [[control_oid, criticality, control_value(optional)]]
86+
# [['1.2.840.113556.1.4.805',true]]
87+
#
88+
def to_ber_control
89+
#if our array does not contain at least one array then wrap it in an array before going forward
90+
ary = self[0].kind_of?(Array) ? self : [self]
91+
ary = ary.collect do |control_sequence|
92+
control_sequence.collect{|element| element.to_ber}.to_ber_sequence.reject_empty_ber_arrays
93+
end
94+
ary.to_ber_sequence.reject_empty_ber_arrays
95+
end
8296
end

lib/net/ber/core_ext/string.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,19 @@ def to_ber_contextspecific(code)
4646
def read_ber(syntax = nil)
4747
StringIO.new(self).read_ber(syntax)
4848
end
49-
49+
5050
##
51-
# Destructively reads a BER object from the string.
51+
# Destructively reads a BER object from the string.
5252
def read_ber!(syntax = nil)
5353
io = StringIO.new(self)
5454

5555
result = io.read_ber(syntax)
5656
self.slice!(0...io.pos)
57-
57+
5858
return result
5959
end
60+
61+
def reject_empty_ber_arrays
62+
self.gsub(/0\000/n,'')
63+
end
6064
end

lib/net/ldap.rb

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,11 @@ class LdapError < StandardError; end
335335
68 => "Entry Already Exists"
336336
}
337337

338-
module LdapControls
339-
PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
338+
module LDAPControls
339+
PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
340+
SORT_REQUEST = "1.2.840.113556.1.4.473"
341+
SORT_RESPONSE = "1.2.840.113556.1.4.474"
342+
DELETE_TREE = "1.2.840.113556.1.4.805"
340343
end
341344

342345
def self.result2string(code) #:nodoc:
@@ -629,11 +632,10 @@ def search(args = {})
629632
yield entry if block_given?
630633
}
631634
else
632-
@result = 0
633635
begin
634636
conn = Net::LDAP::Connection.new(:host => @host, :port => @port,
635637
:encryption => @encryption)
636-
if (@result = conn.bind(args[:auth] || @auth)) == 0
638+
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
637639
@result = conn.search(args) { |entry|
638640
result_set << entry if result_set
639641
yield entry if block_given?
@@ -645,9 +647,9 @@ def search(args = {})
645647
end
646648

647649
if return_result_set
648-
@result == 0 ? result_set : nil
650+
(!@result.nil? && @result.result_code == 0) ? result_set : nil
649651
else
650-
@result == 0
652+
@result
651653
end
652654
end
653655

@@ -721,7 +723,7 @@ def bind(auth = @auth)
721723
end
722724
end
723725

724-
@result == 0
726+
@result
725727
end
726728

727729
# #bind_as is for testing authentication credentials.
@@ -816,14 +818,14 @@ def add(args)
816818
begin
817819
conn = Connection.new(:host => @host, :port => @port,
818820
:encryption => @encryption)
819-
if (@result = conn.bind(args[:auth] || @auth)) == 0
821+
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
820822
@result = conn.add(args)
821823
end
822824
ensure
823825
conn.close if conn
824826
end
825827
end
826-
@result == 0
828+
@result
827829
end
828830

829831
# Modifies the attribute values of a particular entry on the LDAP
@@ -914,14 +916,15 @@ def modify(args)
914916
begin
915917
conn = Connection.new(:host => @host, :port => @port,
916918
:encryption => @encryption)
917-
if (@result = conn.bind(args[:auth] || @auth)) == 0
919+
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
918920
@result = conn.modify(args)
919921
end
920922
ensure
921923
conn.close if conn
922924
end
923925
end
924-
@result == 0
926+
927+
@result
925928
end
926929

927930
# Add a value to an attribute. Takes the full DN of the entry to modify,
@@ -985,14 +988,14 @@ def rename(args)
985988
begin
986989
conn = Connection.new(:host => @host, :port => @port,
987990
:encryption => @encryption)
988-
if (@result = conn.bind(args[:auth] || @auth)) == 0
991+
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
989992
@result = conn.rename(args)
990993
end
991994
ensure
992995
conn.close if conn
993996
end
994997
end
995-
@result == 0
998+
@result
996999
end
9971000
alias_method :modify_rdn, :rename
9981001

@@ -1013,16 +1016,29 @@ def delete(args)
10131016
begin
10141017
conn = Connection.new(:host => @host, :port => @port,
10151018
:encryption => @encryption)
1016-
if (@result = conn.bind(args[:auth] || @auth)) == 0
1019+
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
10171020
@result = conn.delete(args)
10181021
end
10191022
ensure
10201023
conn.close
10211024
end
10221025
end
1023-
@result == 0
1026+
@result
10241027
end
10251028

1029+
# Delete an entry from the LDAP directory along with all subordinate entries.
1030+
# the regular delete method will fail to delete an entry if it has subordinate
1031+
# entries. This method sends an extra control code to tell the LDAP server
1032+
# to do a tree delete. ('1.2.840.113556.1.4.805')
1033+
#
1034+
# Returns True or False to indicate whether the delete succeeded. Extended
1035+
# status information is available by calling #get_operation_result.
1036+
#
1037+
# dn = "[email protected], ou=people, dc=example, dc=com"
1038+
# ldap.delete_tree :dn => dn
1039+
def delete_tree(args)
1040+
delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
1041+
end
10261042
# This method is experimental and subject to change. Return the rootDSE
10271043
# record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
10281044
# the server doesn't return the record.
@@ -1093,7 +1109,7 @@ def search_subschema_entry
10931109
#++
10941110
def paged_searches_supported?
10951111
@server_caps ||= search_root_dse
1096-
@server_caps[:supportedcontrol].include?(Net::LDAP::LdapControls::PagedResults)
1112+
@server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS)
10971113
end
10981114
end # class LDAP
10991115

@@ -1237,7 +1253,7 @@ def bind_simple(auth)
12371253

12381254
(be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
12391255

1240-
pdu.result_code
1256+
pdu
12411257
end
12421258

12431259
#--
@@ -1275,7 +1291,7 @@ def bind_sasl(auth)
12751291
@conn.write request_pkt
12761292

12771293
(be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1278-
return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
1294+
return pdu unless pdu.result_code == 14 # saslBindInProgress
12791295
raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
12801296

12811297
cred = chall.call(pdu.result_server_sasl_creds)
@@ -1315,6 +1331,35 @@ def bind_gss_spnego(auth)
13151331
end
13161332
private :bind_gss_spnego
13171333

1334+
1335+
#--
1336+
# Allow the caller to specify a sort control
1337+
#
1338+
# The format of the sort control needs to be:
1339+
#
1340+
# :sort_control => ["cn"] # just a string
1341+
# or
1342+
# :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
1343+
# or
1344+
# :sort_control => ["givenname","sn"] #multiple strings or arrays
1345+
#
1346+
def encode_sort_controls(sort_definitions)
1347+
return sort_definitions unless sort_definitions
1348+
1349+
sort_control_values = sort_definitions.map do |control|
1350+
control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
1351+
control[0] = String(control[0]).to_ber,
1352+
control[1] = String(control[1]).to_ber,
1353+
control[2] = (control[2] == true).to_ber
1354+
control.to_ber_sequence
1355+
end
1356+
sort_control = [
1357+
Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
1358+
false.to_ber,
1359+
sort_control_values.to_ber_sequence.to_s.to_ber
1360+
].to_ber_sequence
1361+
end
1362+
13181363
#--
13191364
# Alternate implementation, this yields each search entry to the caller as
13201365
# it are received.
@@ -1340,6 +1385,7 @@ def search(args = {})
13401385
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
13411386
raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
13421387

1388+
sort_control = encode_sort_controls(args.fetch(:sort_controls){ false })
13431389
# An interesting value for the size limit would be close to A/D's
13441390
# built-in page limit of 1000 records, but openLDAP newer than version
13451391
# 2.2.0 chokes on anything bigger than 126. You get a silent error that
@@ -1361,7 +1407,7 @@ def search(args = {})
13611407
# to do a root-DSE record search and not do a paged search if the LDAP
13621408
# doesn't support it. Yuck.
13631409
rfc2696_cookie = [126, ""]
1364-
result_code = 0
1410+
result_pdu = nil
13651411
n_results = 0
13661412

13671413
loop {
@@ -1390,17 +1436,18 @@ def search(args = {})
13901436
controls = []
13911437
controls <<
13921438
[
1393-
Net::LDAP::LdapControls::PagedResults.to_ber,
1439+
Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
13941440
# Criticality MUST be false to interoperate with normal LDAPs.
13951441
false.to_ber,
13961442
rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
13971443
].to_ber_sequence if paged_searches_supported
1444+
controls << sort_control if sort_control
13981445
controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
13991446

14001447
pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
14011448
@conn.write pkt
14021449

1403-
result_code = 0
1450+
result_pdu = nil
14041451
controls = []
14051452

14061453
while (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be))
@@ -1417,7 +1464,7 @@ def search(args = {})
14171464
end
14181465
end
14191466
when 5 # search-result
1420-
result_code = pdu.result_code
1467+
result_pdu = pdu
14211468
controls = pdu.result_controls
14221469
if return_referrals && result_code == 10
14231470
if block_given?
@@ -1443,9 +1490,9 @@ def search(args = {})
14431490
# of type OCTET STRING, covered in the default syntax supported by
14441491
# read_ber, so I guess we're ok.
14451492
more_pages = false
1446-
if result_code == 0 and controls
1493+
if result_pdu.result_code == 0 and controls
14471494
controls.each do |c|
1448-
if c.oid == Net::LDAP::LdapControls::PagedResults
1495+
if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
14491496
# just in case some bogus server sends us more than 1 of these.
14501497
more_pages = false
14511498
if c.value and c.value.length > 0
@@ -1462,7 +1509,7 @@ def search(args = {})
14621509
break unless more_pages
14631510
} # loop
14641511

1465-
result_code
1512+
result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search")
14661513
end
14671514

14681515
MODIFY_OPERATIONS = { #:nodoc:
@@ -1502,7 +1549,8 @@ def modify(args)
15021549
@conn.write pkt
15031550

15041551
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 7) or raise Net::LDAP::LdapError, "response missing or invalid"
1505-
pdu.result_code
1552+
1553+
pdu
15061554
end
15071555

15081556
#--
@@ -1523,8 +1571,12 @@ def add(args)
15231571
pkt = [next_msgid.to_ber, request].to_ber_sequence
15241572
@conn.write pkt
15251573

1526-
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 9) or raise Net::LDAP::LdapError, "response missing or invalid"
1527-
pdu.result_code
1574+
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
1575+
(pdu = Net::LDAP::PDU.new(be)) &&
1576+
(pdu.app_tag == 9) or
1577+
raise Net::LDAP::LdapError, "response missing or invalid"
1578+
1579+
pdu
15281580
end
15291581

15301582
#--
@@ -1545,20 +1597,22 @@ def rename args
15451597
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
15461598
(pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == 13) or
15471599
raise Net::LDAP::LdapError.new( "response missing or invalid" )
1548-
pdu.result_code
1600+
1601+
pdu
15491602
end
15501603

15511604
#--
15521605
# TODO, need to support a time limit, in case the server fails to respond.
15531606
#++
15541607
def delete(args)
15551608
dn = args[:dn] or raise "Unable to delete empty DN"
1556-
1609+
controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
15571610
request = dn.to_s.to_ber_application_string(10)
1558-
pkt = [next_msgid.to_ber, request].to_ber_sequence
1611+
pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
15591612
@conn.write pkt
15601613

15611614
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 11) or raise Net::LDAP::LdapError, "response missing or invalid"
1562-
pdu.result_code
1615+
1616+
pdu
15631617
end
15641618
end # class Connection

lib/net/ldap/pdu.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ def result
112112
@ldap_result || {}
113113
end
114114

115+
def error_message
116+
result[:errorMessage] || ""
117+
end
118+
115119
##
116120
# This returns an LDAP result code taken from the PDU, but it will be nil
117121
# if there wasn't a result code. That can easily happen depending on the
@@ -120,6 +124,18 @@ def result_code(code = :resultCode)
120124
@ldap_result and @ldap_result[code]
121125
end
122126

127+
def status
128+
result_code == 0 ? :success : :failure
129+
end
130+
131+
def success?
132+
status == :success
133+
end
134+
135+
def failure?
136+
!success?
137+
end
138+
123139
##
124140
# Return serverSaslCreds, which are only present in BindResponse packets.
125141
#--

0 commit comments

Comments
 (0)