net-ldap 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of net-ldap might be problematic. Click here for more details.
- data/History.txt +7 -6
- data/LICENSE +3 -2
- data/Manifest.txt +13 -5
- data/README.txt +34 -28
- data/Rakefile +115 -11
- data/lib/net-ldap.rb +1 -0
- data/lib/net/ber.rb +52 -514
- data/lib/net/ber/ber_parser.rb +112 -0
- data/lib/net/ldap.rb +486 -540
- data/lib/net/ldap/core_ext/all.rb +43 -0
- data/lib/net/ldap/core_ext/array.rb +42 -0
- data/lib/net/ldap/core_ext/bignum.rb +25 -0
- data/lib/net/ldap/core_ext/false_class.rb +11 -0
- data/lib/net/ldap/core_ext/fixnum.rb +64 -0
- data/lib/net/ldap/core_ext/string.rb +40 -0
- data/lib/net/ldap/core_ext/true_class.rb +11 -0
- data/lib/net/ldap/dataset.rb +64 -73
- data/lib/net/ldap/entry.rb +0 -9
- data/lib/net/ldap/filter.rb +1 -8
- data/lib/net/ldap/pdu.rb +2 -3
- data/lib/net/ldap/psw.rb +31 -38
- data/lib/net/ldif.rb +2 -7
- data/lib/net/snmp.rb +2 -4
- data/spec/integration/ssl_ber_spec.rb +36 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/ber/ber_spec.rb +18 -0
- data/test/common.rb +0 -4
- data/test/test_ber.rb +73 -95
- data/test/test_filter.rb +1 -1
- data/test/test_ldif.rb +1 -1
- data/test/test_snmp.rb +61 -78
- data/testserver/ldapserver.rb +0 -19
- metadata +118 -24
- data/Release-Announcement +0 -95
- data/pre-setup.rb +0 -45
- data/setup.rb +0 -1366
- data/tests/NOTICE.txt +0 -6
- data/tests/testldap.rb +0 -190
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module BER
|
5
|
+
module BERParser
|
6
|
+
VERSION = '0.1.0'
|
7
|
+
|
8
|
+
# The order of these follows the class-codes in BER.
|
9
|
+
# Maybe this should have been a hash.
|
10
|
+
TagClasses = [:universal, :application, :context_specific, :private]
|
11
|
+
|
12
|
+
BuiltinSyntax = Net::BER.compile_syntax( {
|
13
|
+
:universal => {
|
14
|
+
:primitive => {
|
15
|
+
1 => :boolean,
|
16
|
+
2 => :integer,
|
17
|
+
4 => :string,
|
18
|
+
5 => :null,
|
19
|
+
6 => :oid,
|
20
|
+
10 => :integer,
|
21
|
+
13 => :string # (relative OID)
|
22
|
+
},
|
23
|
+
:constructed => {
|
24
|
+
16 => :array,
|
25
|
+
17 => :array
|
26
|
+
}
|
27
|
+
},
|
28
|
+
:context_specific => {
|
29
|
+
:primitive => {
|
30
|
+
10 => :integer
|
31
|
+
}
|
32
|
+
}
|
33
|
+
})
|
34
|
+
|
35
|
+
def read_ber syntax=nil
|
36
|
+
# TODO: clean this up so it works properly with partial
|
37
|
+
# packets coming from streams that don't block when
|
38
|
+
# we ask for more data (like StringIOs). At it is,
|
39
|
+
# this can throw TypeErrors and other nasties.
|
40
|
+
|
41
|
+
id = getbyte or return nil # don't trash this value, we'll use it later
|
42
|
+
|
43
|
+
n = getbyte
|
44
|
+
lengthlength,contentlength = if n <= 127
|
45
|
+
[1,n]
|
46
|
+
else
|
47
|
+
# Replaced the inject because it profiles hot.
|
48
|
+
# j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc}
|
49
|
+
j = 0
|
50
|
+
read( n & 127 ).each_byte {|n1| j = (j << 8) + n1}
|
51
|
+
[1 + (n & 127), j]
|
52
|
+
end
|
53
|
+
|
54
|
+
newobj = read contentlength
|
55
|
+
|
56
|
+
# This exceptionally clever and clear bit of code is verrrry slow.
|
57
|
+
objtype = (syntax && syntax[id]) || BuiltinSyntax[id]
|
58
|
+
|
59
|
+
# == is expensive so sort this if/else so the common cases are at the top.
|
60
|
+
obj = if objtype == :string
|
61
|
+
#(newobj || "").dup
|
62
|
+
s = BerIdentifiedString.new( newobj || "" )
|
63
|
+
s.ber_identifier = id
|
64
|
+
s
|
65
|
+
elsif objtype == :integer
|
66
|
+
j = 0
|
67
|
+
newobj.each_byte {|b| j = (j << 8) + b}
|
68
|
+
j
|
69
|
+
elsif objtype == :oid
|
70
|
+
# cf X.690 pgh 8.19 for an explanation of this algorithm.
|
71
|
+
# Potentially not good enough. We may need a BerIdentifiedOid
|
72
|
+
# as a subclass of BerIdentifiedArray, to get the ber identifier
|
73
|
+
# and also a to_s method that produces the familiar dotted notation.
|
74
|
+
oid = newobj.unpack("w*")
|
75
|
+
f = oid.shift
|
76
|
+
g = if f < 40
|
77
|
+
[0, f]
|
78
|
+
elsif f < 80
|
79
|
+
[1, f-40]
|
80
|
+
else
|
81
|
+
[2, f-80] # f-80 can easily be > 80. What a weird optimization.
|
82
|
+
end
|
83
|
+
oid.unshift g.last
|
84
|
+
oid.unshift g.first
|
85
|
+
oid
|
86
|
+
elsif objtype == :array
|
87
|
+
#seq = []
|
88
|
+
seq = BerIdentifiedArray.new
|
89
|
+
seq.ber_identifier = id
|
90
|
+
sio = StringIO.new( newobj || "" )
|
91
|
+
# Interpret the subobject, but note how the loop
|
92
|
+
# is built: nil ends the loop, but false (a valid
|
93
|
+
# BER value) does not!
|
94
|
+
while (e = sio.read_ber(syntax)) != nil
|
95
|
+
seq << e
|
96
|
+
end
|
97
|
+
seq
|
98
|
+
elsif objtype == :boolean
|
99
|
+
newobj != "\000"
|
100
|
+
elsif objtype == :null
|
101
|
+
n = BerIdentifiedNull.new
|
102
|
+
n.ber_identifier = id
|
103
|
+
n
|
104
|
+
else
|
105
|
+
raise BerError.new( "unsupported object type: id=#{id}" )
|
106
|
+
end
|
107
|
+
|
108
|
+
obj
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/net/ldap.rb
CHANGED
@@ -1,41 +1,15 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
# Net::LDAP for Ruby
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
7
|
-
#
|
8
|
-
# Written and maintained by Francis Cianfrocca, gmail: garbagecat10.
|
9
|
-
#
|
10
|
-
# This program is free software.
|
11
|
-
# You may re-distribute and/or modify this program under the same terms
|
12
|
-
# as Ruby itself: Ruby Distribution License or GNU General Public License.
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# See Net::LDAP for documentation and usage samples.
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
require 'socket'
|
1
|
+
require 'openssl'
|
20
2
|
require 'ostruct'
|
21
3
|
|
22
|
-
begin
|
23
|
-
require 'openssl'
|
24
|
-
$net_ldap_openssl_available = true
|
25
|
-
rescue LoadError
|
26
|
-
end
|
27
|
-
|
28
4
|
require 'net/ber'
|
29
5
|
require 'net/ldap/pdu'
|
30
6
|
require 'net/ldap/filter'
|
31
7
|
require 'net/ldap/dataset'
|
32
8
|
require 'net/ldap/psw'
|
33
9
|
require 'net/ldap/entry'
|
34
|
-
|
10
|
+
require 'net/ldap/core_ext/all'
|
35
11
|
|
36
12
|
module Net
|
37
|
-
|
38
|
-
|
39
13
|
# == Net::LDAP
|
40
14
|
#
|
41
15
|
# This library provides a pure-Ruby implementation of the
|
@@ -257,46 +231,42 @@ module Net
|
|
257
231
|
# and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection
|
258
232
|
# to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open
|
259
233
|
# closes the connection on completion of the block.
|
260
|
-
#
|
261
|
-
|
262
234
|
class LDAP
|
263
|
-
|
264
235
|
class LdapError < StandardError; end
|
265
236
|
|
266
|
-
VERSION = "0.0
|
267
|
-
|
237
|
+
VERSION = "0.1.0"
|
268
238
|
|
269
239
|
SearchScope_BaseObject = 0
|
270
240
|
SearchScope_SingleLevel = 1
|
271
241
|
SearchScope_WholeSubtree = 2
|
272
242
|
SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree]
|
273
243
|
|
274
|
-
AsnSyntax = BER.compile_syntax({
|
244
|
+
AsnSyntax = Net::BER.compile_syntax({
|
275
245
|
:application => {
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
246
|
+
:primitive => {
|
247
|
+
2 => :null # UnbindRequest body
|
248
|
+
},
|
249
|
+
:constructed => {
|
250
|
+
0 => :array, # BindRequest
|
251
|
+
1 => :array, # BindResponse
|
252
|
+
2 => :array, # UnbindRequest
|
253
|
+
3 => :array, # SearchRequest
|
254
|
+
4 => :array, # SearchData
|
255
|
+
5 => :array, # SearchResult
|
256
|
+
6 => :array, # ModifyRequest
|
257
|
+
7 => :array, # ModifyResponse
|
258
|
+
8 => :array, # AddRequest
|
259
|
+
9 => :array, # AddResponse
|
260
|
+
10 => :array, # DelRequest
|
261
|
+
11 => :array, # DelResponse
|
262
|
+
12 => :array, # ModifyRdnRequest
|
263
|
+
13 => :array, # ModifyRdnResponse
|
264
|
+
14 => :array, # CompareRequest
|
265
|
+
15 => :array, # CompareResponse
|
266
|
+
16 => :array, # AbandonRequest
|
267
|
+
19 => :array, # SearchResultReferral
|
268
|
+
24 => :array, # Unsolicited Notification
|
269
|
+
}
|
300
270
|
},
|
301
271
|
:context_specific => {
|
302
272
|
:primitive => {
|
@@ -348,23 +318,17 @@ module Net
|
|
348
318
|
68 => "Entry Already Exists"
|
349
319
|
}
|
350
320
|
|
351
|
-
|
352
321
|
module LdapControls
|
353
322
|
PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
|
354
323
|
end
|
355
324
|
|
356
|
-
|
357
|
-
#
|
358
325
|
# LDAP::result2string
|
359
|
-
#
|
360
326
|
def LDAP::result2string code # :nodoc:
|
361
327
|
ResultStrings[code] || "unknown result (#{code})"
|
362
328
|
end
|
363
329
|
|
364
|
-
|
365
330
|
attr_accessor :host, :port, :base
|
366
331
|
|
367
|
-
|
368
332
|
# Instantiate an object of type Net::LDAP to perform directory operations.
|
369
333
|
# This constructor takes a Hash containing arguments, all of which are either optional or may be specified later with other methods as described below. The following arguments
|
370
334
|
# are supported:
|
@@ -478,12 +442,11 @@ module Net
|
|
478
442
|
def encryption args
|
479
443
|
case args
|
480
444
|
when :simple_tls, :start_tls
|
481
|
-
|
445
|
+
args = {:method => args}
|
482
446
|
end
|
483
447
|
@encryption = args
|
484
448
|
end
|
485
449
|
|
486
|
-
|
487
450
|
# #open takes the same parameters as #new. #open makes a network connection to the
|
488
451
|
# LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
|
489
452
|
# Within the block, you can call any of the instance methods of Net::LDAP to
|
@@ -504,30 +467,31 @@ module Net
|
|
504
467
|
ldap1.open {|ldap| yield ldap }
|
505
468
|
end
|
506
469
|
|
507
|
-
# Returns a meaningful result any time after
|
508
|
-
#
|
509
|
-
#
|
510
|
-
#
|
511
|
-
#
|
470
|
+
# Returns a meaningful result any time after a protocol operation
|
471
|
+
# (#bind, #search, #add, #modify, #rename, #delete) has completed.
|
472
|
+
# It returns an #OpenStruct containing an LDAP result code (0 means
|
473
|
+
# success), and a human-readable string.
|
474
|
+
#
|
512
475
|
# unless ldap.bind
|
513
476
|
# puts "Result: #{ldap.get_operation_result.code}"
|
514
477
|
# puts "Message: #{ldap.get_operation_result.message}"
|
515
478
|
# end
|
516
479
|
#
|
517
|
-
# Certain operations return additional information, accessible through
|
518
|
-
# of the object returned from #get_operation_result. Check
|
519
|
-
#
|
480
|
+
# Certain operations return additional information, accessible through
|
481
|
+
# members of the object returned from #get_operation_result. Check
|
482
|
+
# #get_operation_result.error_message and
|
483
|
+
# #get_operation_result.matched_dn.
|
520
484
|
#
|
521
485
|
#--
|
522
|
-
# Modified the implementation, 20Mar07. We might get a hash of LDAP
|
523
|
-
# instead of a simple numeric code.
|
524
|
-
|
486
|
+
# Modified the implementation, 20Mar07. We might get a hash of LDAP
|
487
|
+
# response codes instead of a simple numeric code.
|
488
|
+
#++
|
525
489
|
def get_operation_result
|
526
490
|
os = OpenStruct.new
|
527
491
|
if @result.is_a?(Hash)
|
528
|
-
|
529
|
-
|
530
|
-
|
492
|
+
os.code = (@result[:resultCode] || "").to_i
|
493
|
+
os.error_message = @result[:errorMessage]
|
494
|
+
os.matched_dn = @result[:matchedDN]
|
531
495
|
elsif @result
|
532
496
|
os.code = @result
|
533
497
|
else
|
@@ -537,7 +501,6 @@ module Net
|
|
537
501
|
os
|
538
502
|
end
|
539
503
|
|
540
|
-
|
541
504
|
# Opens a network connection to the server and then
|
542
505
|
# passes <tt>self</tt> to the caller-supplied block. The connection is
|
543
506
|
# closed when the block completes. Used for executing multiple
|
@@ -561,6 +524,7 @@ module Net
|
|
561
524
|
# We then pass self to the caller's block, where he will execute
|
562
525
|
# his LDAP operations. Of course they will all generate auth failures
|
563
526
|
# if the bind was unsuccessful.
|
527
|
+
#++
|
564
528
|
def open
|
565
529
|
raise LdapError.new( "open already in progress" ) if @open_connection
|
566
530
|
begin
|
@@ -573,7 +537,6 @@ module Net
|
|
573
537
|
end
|
574
538
|
end
|
575
539
|
|
576
|
-
|
577
540
|
# Searches the LDAP directory for directory entries.
|
578
541
|
# Takes a hash argument with parameters. Supported parameters include:
|
579
542
|
# * :base (a string specifying the tree-base for the search);
|
@@ -651,7 +614,7 @@ module Net
|
|
651
614
|
# handle DNs. Change it to a plain array. Eventually we may
|
652
615
|
# want to return a Dataset object that delegates to an internal
|
653
616
|
# array, so we can provide sort methods and what-not.
|
654
|
-
|
617
|
+
#++
|
655
618
|
def search args = {}
|
656
619
|
unless args[:ignore_server_caps]
|
657
620
|
args[:paged_searches_supported] = paged_searches_supported?
|
@@ -744,8 +707,8 @@ module Net
|
|
744
707
|
# If there is an @open_connection, then perform the bind
|
745
708
|
# on it. Otherwise, connect, bind, and disconnect.
|
746
709
|
# The latter operation is obviously useful only as an auth check.
|
747
|
-
|
748
|
-
def bind
|
710
|
+
#++
|
711
|
+
def bind(auth=@auth)
|
749
712
|
if @open_connection
|
750
713
|
@result = @open_connection.bind auth
|
751
714
|
else
|
@@ -760,7 +723,6 @@ module Net
|
|
760
723
|
@result == 0
|
761
724
|
end
|
762
725
|
|
763
|
-
|
764
726
|
#
|
765
727
|
# #bind_as is for testing authentication credentials.
|
766
728
|
#
|
@@ -820,7 +782,6 @@ module Net
|
|
820
782
|
result
|
821
783
|
end
|
822
784
|
|
823
|
-
|
824
785
|
# Adds a new entry to the remote LDAP server.
|
825
786
|
# Supported arguments:
|
826
787
|
# :dn :: Full DN of the new entry
|
@@ -847,7 +808,7 @@ module Net
|
|
847
808
|
#--
|
848
809
|
# Provisional modification: Connection#add returns a full hash with LDAP status values,
|
849
810
|
# instead of the simple result number we're used to getting.
|
850
|
-
|
811
|
+
#++
|
851
812
|
def add args
|
852
813
|
if @open_connection
|
853
814
|
@result = @open_connection.add( args )
|
@@ -865,7 +826,6 @@ module Net
|
|
865
826
|
@result == 0
|
866
827
|
end
|
867
828
|
|
868
|
-
|
869
829
|
# Modifies the attribute values of a particular entry on the LDAP directory.
|
870
830
|
# Takes a hash with arguments. Supported arguments are:
|
871
831
|
# :dn :: (the full DN of the entry whose attributes are to be modified)
|
@@ -963,7 +923,6 @@ module Net
|
|
963
923
|
@result == 0
|
964
924
|
end
|
965
925
|
|
966
|
-
|
967
926
|
# Add a value to an attribute.
|
968
927
|
# Takes the full DN of the entry to modify,
|
969
928
|
# the name (Symbol or String) of the attribute, and the value (String or
|
@@ -1017,7 +976,6 @@ module Net
|
|
1017
976
|
modify :dn => dn, :operations => [[:delete, attribute, nil]]
|
1018
977
|
end
|
1019
978
|
|
1020
|
-
|
1021
979
|
# Rename an entry on the remote DIS by changing the last RDN of its DN.
|
1022
980
|
# _Documentation_ _stub_
|
1023
981
|
#
|
@@ -1071,7 +1029,6 @@ module Net
|
|
1071
1029
|
@result == 0
|
1072
1030
|
end
|
1073
1031
|
|
1074
|
-
|
1075
1032
|
# (Experimental, subject to change).
|
1076
1033
|
# Return the rootDSE record from the LDAP server as a Net::LDAP::Entry, or an
|
1077
1034
|
# empty Entry if the server doesn't return the record.
|
@@ -1084,7 +1041,7 @@ module Net
|
|
1084
1041
|
# We may be called by #search itself, which may need to determine things like paged
|
1085
1042
|
# search capabilities. So to avoid an infinite regress, set :ignore_server_caps,
|
1086
1043
|
# which prevents us getting called recursively.
|
1087
|
-
|
1044
|
+
#++
|
1088
1045
|
def search_root_dse
|
1089
1046
|
rs = search(
|
1090
1047
|
:ignore_server_caps=>true,
|
@@ -1095,519 +1052,508 @@ module Net
|
|
1095
1052
|
(rs and rs.first) or Entry.new
|
1096
1053
|
end
|
1097
1054
|
|
1055
|
+
# Return the root Subschema record from the LDAP server as a Net::LDAP::Entry,
|
1056
|
+
# or an empty Entry if the server doesn't return the record. On success, the
|
1057
|
+
# Net::LDAP::Entry returned from this call will have the attributes :dn,
|
1058
|
+
# :objectclasses, and :attributetypes. If there is an error, call #get_operation_result
|
1059
|
+
# for more information.
|
1060
|
+
#
|
1061
|
+
# ldap = Net::LDAP.new
|
1062
|
+
# ldap.host = "your.ldap.host"
|
1063
|
+
# ldap.auth "your-user-dn", "your-psw"
|
1064
|
+
# subschema_entry = ldap.search_subschema_entry
|
1065
|
+
#
|
1066
|
+
# subschema_entry.attributetypes.each do |attrtype|
|
1067
|
+
# # your code
|
1068
|
+
# end
|
1069
|
+
#
|
1070
|
+
# subschema_entry.objectclasses.each do |attrtype|
|
1071
|
+
# # your code
|
1072
|
+
# end
|
1073
|
+
#--
|
1074
|
+
# cf. RFC4512 section 4, particulary graff 4.4.
|
1075
|
+
# The :dn attribute in the returned Entry is the subschema name as returned from
|
1076
|
+
# the server.
|
1077
|
+
# Set :ignore_server_caps, see the notes in search_root_dse.
|
1078
|
+
#++
|
1079
|
+
def search_subschema_entry
|
1080
|
+
rs = search(
|
1081
|
+
:ignore_server_caps=>true,
|
1082
|
+
:base=>"",
|
1083
|
+
:scope=>SearchScope_BaseObject,
|
1084
|
+
:attributes=>[:subschemaSubentry]
|
1085
|
+
)
|
1086
|
+
return Entry.new unless (rs and rs.first)
|
1087
|
+
subschema_name = rs.first.subschemasubentry
|
1088
|
+
return Entry.new unless (subschema_name and subschema_name.first)
|
1098
1089
|
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
# ldap.host = "your.ldap.host"
|
1107
|
-
# ldap.auth "your-user-dn", "your-psw"
|
1108
|
-
# subschema_entry = ldap.search_subschema_entry
|
1109
|
-
#
|
1110
|
-
# subschema_entry.attributetypes.each do |attrtype|
|
1111
|
-
# # your code
|
1112
|
-
# end
|
1113
|
-
#
|
1114
|
-
# subschema_entry.objectclasses.each do |attrtype|
|
1115
|
-
# # your code
|
1116
|
-
# end
|
1117
|
-
#--
|
1118
|
-
# cf. RFC4512 section 4, particulary graff 4.4.
|
1119
|
-
# The :dn attribute in the returned Entry is the subschema name as returned from
|
1120
|
-
# the server.
|
1121
|
-
# Set :ignore_server_caps, see the notes in search_root_dse.
|
1122
|
-
#
|
1123
|
-
def search_subschema_entry
|
1124
|
-
rs = search(
|
1125
|
-
:ignore_server_caps=>true,
|
1126
|
-
:base=>"",
|
1127
|
-
:scope=>SearchScope_BaseObject,
|
1128
|
-
:attributes=>[:subschemaSubentry]
|
1129
|
-
)
|
1130
|
-
return Entry.new unless (rs and rs.first)
|
1131
|
-
subschema_name = rs.first.subschemasubentry
|
1132
|
-
return Entry.new unless (subschema_name and subschema_name.first)
|
1133
|
-
|
1134
|
-
rs = search(
|
1135
|
-
:ignore_server_caps=>true,
|
1136
|
-
:base=>subschema_name.first,
|
1137
|
-
:scope=>SearchScope_BaseObject,
|
1138
|
-
:filter=>"objectclass=subschema",
|
1139
|
-
:attributes=>[:objectclasses, :attributetypes]
|
1140
|
-
)
|
1141
|
-
|
1142
|
-
(rs and rs.first) or Entry.new
|
1143
|
-
end
|
1090
|
+
rs = search(
|
1091
|
+
:ignore_server_caps=>true,
|
1092
|
+
:base=>subschema_name.first,
|
1093
|
+
:scope=>SearchScope_BaseObject,
|
1094
|
+
:filter=>"objectclass=subschema",
|
1095
|
+
:attributes=>[:objectclasses, :attributetypes]
|
1096
|
+
)
|
1144
1097
|
|
1098
|
+
(rs and rs.first) or Entry.new
|
1099
|
+
end
|
1145
1100
|
|
1146
1101
|
#--
|
1147
1102
|
# Convenience method to query server capabilities.
|
1148
1103
|
# Only do this once per Net::LDAP object.
|
1149
1104
|
# Note, we call a search, and we might be called from inside a search!
|
1150
1105
|
# MUST refactor the root_dse call out.
|
1106
|
+
#++
|
1151
1107
|
def paged_searches_supported?
|
1152
1108
|
@server_caps ||= search_root_dse
|
1153
1109
|
@server_caps[:supportedcontrol].include?(LdapControls::PagedResults)
|
1154
1110
|
end
|
1155
|
-
|
1156
1111
|
end # class LDAP
|
1157
1112
|
|
1158
|
-
|
1159
|
-
|
1160
1113
|
class LDAP
|
1161
|
-
|
1162
|
-
|
1114
|
+
# This is a private class used internally by the library. It should not
|
1115
|
+
# be called by user code.
|
1116
|
+
class Connection # :nodoc:
|
1117
|
+
LdapVersion = 3
|
1118
|
+
MaxSaslChallenges = 10
|
1163
1119
|
|
1164
|
-
|
1165
|
-
|
1120
|
+
def initialize server
|
1121
|
+
begin
|
1122
|
+
@conn = TCPSocket.new( server[:host], server[:port] )
|
1123
|
+
rescue
|
1124
|
+
raise LdapError.new( "no connection to server" )
|
1125
|
+
end
|
1166
1126
|
|
1127
|
+
if server[:encryption]
|
1128
|
+
setup_encryption server[:encryption]
|
1129
|
+
end
|
1167
1130
|
|
1168
|
-
|
1169
|
-
# initialize
|
1170
|
-
#
|
1171
|
-
def initialize server
|
1172
|
-
begin
|
1173
|
-
@conn = TCPSocket.new( server[:host], server[:port] )
|
1174
|
-
rescue
|
1175
|
-
raise LdapError.new( "no connection to server" )
|
1131
|
+
yield self if block_given?
|
1176
1132
|
end
|
1177
1133
|
|
1178
|
-
|
1179
|
-
|
1134
|
+
module GetbyteForSSLSocket
|
1135
|
+
def getbyte
|
1136
|
+
getc.ord
|
1137
|
+
end
|
1180
1138
|
end
|
1181
1139
|
|
1182
|
-
|
1183
|
-
end
|
1184
|
-
|
1185
|
-
|
1186
|
-
#--
|
1187
|
-
# Helper method called only from new, and only after we have a successfully-opened
|
1188
|
-
# @conn instance variable, which is a TCP connection.
|
1189
|
-
# Depending on the received arguments, we establish SSL, potentially replacing
|
1190
|
-
# the value of @conn accordingly.
|
1191
|
-
# Don't generate any errors here if no encryption is requested.
|
1192
|
-
# DO raise LdapError objects if encryption is requested and we have trouble setting
|
1193
|
-
# it up. That includes if OpenSSL is not set up on the machine. (Question:
|
1194
|
-
# how does the Ruby OpenSSL wrapper react in that case?)
|
1195
|
-
# DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
|
1196
|
-
# to the user. That should make it easier for us to debug the problem reports.
|
1197
|
-
# Presumably (hopefully?) that will also produce recognizable errors if someone
|
1198
|
-
# tries to use this on a machine without OpenSSL.
|
1199
|
-
#
|
1200
|
-
# The simple_tls method is intended as the simplest, stupidest, easiest solution
|
1201
|
-
# for people who want nothing more than encrypted comms with the LDAP server.
|
1202
|
-
# It doesn't do any server-cert validation and requires nothing in the way
|
1203
|
-
# of key files and root-cert files, etc etc.
|
1204
|
-
# OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
|
1205
|
-
# TCPSocket object.
|
1206
|
-
#
|
1207
|
-
# The start_tls method is supported by many servers over the standard LDAP port.
|
1208
|
-
# It does not require an alternative port for encrypted communications, as with
|
1209
|
-
# simple_tls.
|
1210
|
-
# Thanks for Kouhei Sutou for generously contributing the :start_tls path.
|
1211
|
-
#
|
1212
|
-
def setup_encryption args
|
1213
|
-
case args[:method]
|
1214
|
-
when :simple_tls
|
1215
|
-
raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
|
1140
|
+
def self.wrap_with_ssl(io)
|
1216
1141
|
ctx = OpenSSL::SSL::SSLContext.new
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
request = [StartTlsOid.to_ber].to_ber_appsequence( Net::LdapPdu::ExtendedRequest )
|
1225
|
-
request_pkt = [msgid, request].to_ber_sequence
|
1226
|
-
@conn.write request_pkt
|
1227
|
-
be = @conn.read_ber(AsnSyntax)
|
1228
|
-
raise LdapError.new("no start_tls result") if be.nil?
|
1229
|
-
pdu = Net::LdapPdu.new(be)
|
1230
|
-
raise LdapError.new("no start_tls result") if pdu.nil?
|
1231
|
-
if pdu.result_code.zero?
|
1232
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
1233
|
-
@conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
|
1234
|
-
@conn.connect
|
1235
|
-
@conn.sync_close = true
|
1236
|
-
else
|
1237
|
-
raise LdapError.new("start_tls failed: #{pdu.result_code}")
|
1238
|
-
end
|
1239
|
-
else
|
1240
|
-
raise LdapError.new( "unsupported encryption method #{args[:method]}" )
|
1142
|
+
conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
|
1143
|
+
conn.connect
|
1144
|
+
conn.sync_close = true
|
1145
|
+
|
1146
|
+
conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
|
1147
|
+
|
1148
|
+
conn
|
1241
1149
|
end
|
1242
|
-
end
|
1243
|
-
|
1244
|
-
#--
|
1245
|
-
# close
|
1246
|
-
# This is provided as a convenience method to make
|
1247
|
-
# sure a connection object gets closed without waiting
|
1248
|
-
# for a GC to happen. Clients shouldn't have to call it,
|
1249
|
-
# but perhaps it will come in handy someday.
|
1250
|
-
def close
|
1251
|
-
@conn.close
|
1252
|
-
@conn = nil
|
1253
|
-
end
|
1254
|
-
|
1255
|
-
#--
|
1256
|
-
# next_msgid
|
1257
|
-
#
|
1258
|
-
def next_msgid
|
1259
|
-
@msgid ||= 0
|
1260
|
-
@msgid += 1
|
1261
|
-
end
|
1262
1150
|
|
1151
|
+
#--
|
1152
|
+
# Helper method called only from new, and only after we have a successfully-opened
|
1153
|
+
# @conn instance variable, which is a TCP connection.
|
1154
|
+
# Depending on the received arguments, we establish SSL, potentially replacing
|
1155
|
+
# the value of @conn accordingly.
|
1156
|
+
# Don't generate any errors here if no encryption is requested.
|
1157
|
+
# DO raise LdapError objects if encryption is requested and we have trouble setting
|
1158
|
+
# it up. That includes if OpenSSL is not set up on the machine. (Question:
|
1159
|
+
# how does the Ruby OpenSSL wrapper react in that case?)
|
1160
|
+
# DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
|
1161
|
+
# to the user. That should make it easier for us to debug the problem reports.
|
1162
|
+
# Presumably (hopefully?) that will also produce recognizable errors if someone
|
1163
|
+
# tries to use this on a machine without OpenSSL.
|
1164
|
+
#
|
1165
|
+
# The simple_tls method is intended as the simplest, stupidest, easiest solution
|
1166
|
+
# for people who want nothing more than encrypted comms with the LDAP server.
|
1167
|
+
# It doesn't do any server-cert validation and requires nothing in the way
|
1168
|
+
# of key files and root-cert files, etc etc.
|
1169
|
+
# OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
|
1170
|
+
# TCPSocket object.
|
1171
|
+
#
|
1172
|
+
# The start_tls method is supported by many servers over the standard LDAP port.
|
1173
|
+
# It does not require an alternative port for encrypted communications, as with
|
1174
|
+
# simple_tls.
|
1175
|
+
# Thanks for Kouhei Sutou for generously contributing the :start_tls path.
|
1176
|
+
#++
|
1177
|
+
def setup_encryption args
|
1178
|
+
case args[:method]
|
1179
|
+
when :simple_tls
|
1180
|
+
@conn = self.class.wrap_with_ssl(@conn)
|
1181
|
+
# additional branches requiring server validation and peer certs, etc. go here.
|
1182
|
+
when :start_tls
|
1183
|
+
msgid = next_msgid.to_ber
|
1184
|
+
request = [StartTlsOid.to_ber].to_ber_appsequence( Net::LdapPdu::ExtendedRequest )
|
1185
|
+
request_pkt = [msgid, request].to_ber_sequence
|
1186
|
+
@conn.write request_pkt
|
1187
|
+
be = @conn.read_ber(AsnSyntax)
|
1188
|
+
raise LdapError.new("no start_tls result") if be.nil?
|
1189
|
+
pdu = Net::LdapPdu.new(be)
|
1190
|
+
raise LdapError.new("no start_tls result") if pdu.nil?
|
1191
|
+
if pdu.result_code.zero?
|
1192
|
+
@conn = self.class.wrap_with_ssl(@conn)
|
1193
|
+
else
|
1194
|
+
raise LdapError.new("start_tls failed: #{pdu.result_code}")
|
1195
|
+
end
|
1196
|
+
else
|
1197
|
+
raise LdapError.new( "unsupported encryption method #{args[:method]}" )
|
1198
|
+
end
|
1199
|
+
end
|
1263
1200
|
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
bind_gss_spnego( auth )
|
1275
|
-
else
|
1276
|
-
raise LdapError.new( "unsupported auth method (#{meth})" )
|
1201
|
+
#--
|
1202
|
+
# close
|
1203
|
+
# This is provided as a convenience method to make
|
1204
|
+
# sure a connection object gets closed without waiting
|
1205
|
+
# for a GC to happen. Clients shouldn't have to call it,
|
1206
|
+
# but perhaps it will come in handy someday.
|
1207
|
+
#++
|
1208
|
+
def close
|
1209
|
+
@conn.close
|
1210
|
+
@conn = nil
|
1277
1211
|
end
|
1278
|
-
end
|
1279
1212
|
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
user,psw = if auth[:method] == :simple
|
1287
|
-
[auth[:username] || auth[:dn], auth[:password]]
|
1288
|
-
else
|
1289
|
-
["",""]
|
1213
|
+
#--
|
1214
|
+
# next_msgid
|
1215
|
+
#++
|
1216
|
+
def next_msgid
|
1217
|
+
@msgid ||= 0
|
1218
|
+
@msgid += 1
|
1290
1219
|
end
|
1291
1220
|
|
1292
|
-
|
1221
|
+
#--
|
1222
|
+
# bind
|
1223
|
+
#++
|
1224
|
+
def bind auth
|
1225
|
+
meth = auth[:method]
|
1226
|
+
if [:simple, :anonymous, :anon].include?( meth )
|
1227
|
+
bind_simple auth
|
1228
|
+
elsif meth == :sasl
|
1229
|
+
bind_sasl( auth )
|
1230
|
+
elsif meth == :gss_spnego
|
1231
|
+
bind_gss_spnego( auth )
|
1232
|
+
else
|
1233
|
+
raise LdapError.new( "unsupported auth method (#{meth})" )
|
1234
|
+
end
|
1235
|
+
end
|
1293
1236
|
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1237
|
+
#--
|
1238
|
+
# bind_simple
|
1239
|
+
# Implements a simple user/psw authentication.
|
1240
|
+
# Accessed by calling #bind with a method of :simple or :anonymous.
|
1241
|
+
#++
|
1242
|
+
def bind_simple auth
|
1243
|
+
user,psw = if auth[:method] == :simple
|
1244
|
+
[auth[:username] || auth[:dn], auth[:password]]
|
1245
|
+
else
|
1246
|
+
["",""]
|
1247
|
+
end
|
1298
1248
|
|
1299
|
-
|
1300
|
-
pdu.result_code
|
1301
|
-
end
|
1249
|
+
raise LdapError.new( "invalid binding information" ) unless (user && psw)
|
1302
1250
|
|
1303
|
-
#--
|
1304
|
-
# bind_sasl
|
1305
|
-
# Required parameters: :mechanism, :initial_credential and :challenge_response
|
1306
|
-
# Mechanism is a string value that will be passed in the SASL-packet's "mechanism" field.
|
1307
|
-
# Initial credential is most likely a string. It's passed in the initial BindRequest
|
1308
|
-
# that goes to the server. In some protocols, it may be empty.
|
1309
|
-
# Challenge-response is a Ruby proc that takes a single parameter and returns an object
|
1310
|
-
# that will typically be a string. The challenge-response block is called when the server
|
1311
|
-
# returns a BindResponse with a result code of 14 (saslBindInProgress). The challenge-response
|
1312
|
-
# block receives a parameter containing the data returned by the server in the saslServerCreds
|
1313
|
-
# field of the LDAP BindResponse packet. The challenge-response block may be called multiple
|
1314
|
-
# times during the course of a SASL authentication, and each time it must return a value
|
1315
|
-
# that will be passed back to the server as the credential data in the next BindRequest packet.
|
1316
|
-
#
|
1317
|
-
def bind_sasl auth
|
1318
|
-
mech,cred,chall = auth[:mechanism],auth[:initial_credential],auth[:challenge_response]
|
1319
|
-
raise LdapError.new( "invalid binding information" ) unless (mech && cred && chall)
|
1320
|
-
|
1321
|
-
n = 0
|
1322
|
-
loop {
|
1323
1251
|
msgid = next_msgid.to_ber
|
1324
|
-
|
1325
|
-
request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
|
1252
|
+
request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
|
1326
1253
|
request_pkt = [msgid, request].to_ber_sequence
|
1327
1254
|
@conn.write request_pkt
|
1328
1255
|
|
1329
1256
|
(be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
|
1330
|
-
|
1331
|
-
|
1257
|
+
pdu.result_code
|
1258
|
+
end
|
1332
1259
|
|
1333
|
-
|
1334
|
-
|
1260
|
+
#--
|
1261
|
+
# bind_sasl
|
1262
|
+
# Required parameters: :mechanism, :initial_credential and :challenge_response
|
1263
|
+
# Mechanism is a string value that will be passed in the SASL-packet's "mechanism" field.
|
1264
|
+
# Initial credential is most likely a string. It's passed in the initial BindRequest
|
1265
|
+
# that goes to the server. In some protocols, it may be empty.
|
1266
|
+
# Challenge-response is a Ruby proc that takes a single parameter and returns an object
|
1267
|
+
# that will typically be a string. The challenge-response block is called when the server
|
1268
|
+
# returns a BindResponse with a result code of 14 (saslBindInProgress). The challenge-response
|
1269
|
+
# block receives a parameter containing the data returned by the server in the saslServerCreds
|
1270
|
+
# field of the LDAP BindResponse packet. The challenge-response block may be called multiple
|
1271
|
+
# times during the course of a SASL authentication, and each time it must return a value
|
1272
|
+
# that will be passed back to the server as the credential data in the next BindRequest packet.
|
1273
|
+
#++
|
1274
|
+
def bind_sasl auth
|
1275
|
+
mech,cred,chall = auth[:mechanism],auth[:initial_credential],auth[:challenge_response]
|
1276
|
+
raise LdapError.new( "invalid binding information" ) unless (mech && cred && chall)
|
1277
|
+
|
1278
|
+
n = 0
|
1279
|
+
loop {
|
1280
|
+
msgid = next_msgid.to_ber
|
1281
|
+
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
|
1282
|
+
request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
|
1283
|
+
request_pkt = [msgid, request].to_ber_sequence
|
1284
|
+
@conn.write request_pkt
|
1285
|
+
|
1286
|
+
(be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
|
1287
|
+
return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
|
1288
|
+
raise LdapError.new("sasl-challenge overflow") if ((n += 1) > MaxSaslChallenges)
|
1289
|
+
|
1290
|
+
cred = chall.call( pdu.result_server_sasl_creds )
|
1291
|
+
}
|
1335
1292
|
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1293
|
+
raise LdapError.new( "why are we here?")
|
1294
|
+
end
|
1295
|
+
private :bind_sasl
|
1296
|
+
|
1297
|
+
#--
|
1298
|
+
# bind_gss_spnego
|
1299
|
+
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
|
1300
|
+
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to integrate it without
|
1301
|
+
# introducing an external dependency.
|
1302
|
+
# This authentication method is accessed by calling #bind with a :method parameter of
|
1303
|
+
# :gss_spnego. It requires :username and :password attributes, just like the :simple
|
1304
|
+
# authentication method. It performs a GSS-SPNEGO authentication with the server, which
|
1305
|
+
# is presumed to be a Microsoft Active Directory.
|
1306
|
+
#++
|
1307
|
+
def bind_gss_spnego auth
|
1308
|
+
require 'ntlm.rb'
|
1309
|
+
|
1310
|
+
user,psw = [auth[:username] || auth[:dn], auth[:password]]
|
1311
|
+
raise LdapError.new( "invalid binding information" ) unless (user && psw)
|
1312
|
+
|
1313
|
+
nego = proc {|challenge|
|
1314
|
+
t2_msg = NTLM::Message.parse( challenge )
|
1315
|
+
t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} )
|
1316
|
+
t3_msg.serialize
|
1317
|
+
}
|
1339
1318
|
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1319
|
+
bind_sasl( {
|
1320
|
+
:method => :sasl,
|
1321
|
+
:mechanism => "GSS-SPNEGO",
|
1322
|
+
:initial_credential => NTLM::Message::Type1.new.serialize,
|
1323
|
+
:challenge_response => nego
|
1324
|
+
})
|
1325
|
+
end
|
1326
|
+
private :bind_gss_spnego
|
1327
|
+
|
1328
|
+
#--
|
1329
|
+
# search
|
1330
|
+
# Alternate implementation, this yields each search entry to the caller
|
1331
|
+
# as it are received.
|
1332
|
+
# TODO, certain search parameters are hardcoded.
|
1333
|
+
# TODO, if we mis-parse the server results or the results are wrong, we can block
|
1334
|
+
# forever. That's because we keep reading results until we get a type-5 packet,
|
1335
|
+
# which might never come. We need to support the time-limit in the protocol.
|
1336
|
+
#--
|
1337
|
+
# WARNING: this code substantially recapitulates the searchx method.
|
1338
|
+
#
|
1339
|
+
# 02May06: Well, I added support for RFC-2696-style paged searches.
|
1340
|
+
# This is used on all queries because the extension is marked non-critical.
|
1341
|
+
# As far as I know, only A/D uses this, but it's required for A/D. Otherwise
|
1342
|
+
# you won't get more than 1000 results back from a query.
|
1343
|
+
# This implementation is kindof clunky and should probably be refactored.
|
1344
|
+
# Also, is it my imagination, or are A/Ds the slowest directory servers ever???
|
1345
|
+
# OpenLDAP newer than version 2.2.0 supports paged searches.
|
1346
|
+
#++
|
1347
|
+
def search args = {}
|
1348
|
+
search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
|
1349
|
+
search_filter = Filter.construct(search_filter) if search_filter.is_a?(String)
|
1350
|
+
search_base = (args && args[:base]) || "dc=example,dc=com"
|
1351
|
+
search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
|
1352
|
+
return_referrals = args && args[:return_referrals] == true
|
1353
|
+
sizelimit = (args && args[:size].to_i) || 0
|
1354
|
+
raise LdapError.new( "invalid search-size" ) unless sizelimit >= 0
|
1355
|
+
paged_searches_supported = (args && args[:paged_searches_supported])
|
1356
|
+
|
1357
|
+
attributes_only = (args and args[:attributes_only] == true)
|
1358
|
+
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
|
1359
|
+
raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
|
1360
|
+
|
1361
|
+
# An interesting value for the size limit would be close to A/D's built-in
|
1362
|
+
# page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
|
1363
|
+
# on anything bigger than 126. You get a silent error that is easily visible
|
1364
|
+
# by running slapd in debug mode. Go figure.
|
1365
|
+
#
|
1366
|
+
# Changed this around 06Sep06 to support a caller-specified search-size limit.
|
1367
|
+
# Because we ALWAYS do paged searches, we have to work around the problem that
|
1368
|
+
# it's not legal to specify a "normal" sizelimit (in the body of the search request)
|
1369
|
+
# that is larger than the page size we're requesting. Unfortunately, I have the
|
1370
|
+
# feeling that this will break with LDAP servers that don't support paged searches!!!
|
1371
|
+
# (Because we pass zero as the sizelimit on search rounds when the remaining limit
|
1372
|
+
# is larger than our max page size of 126. In these cases, I think the caller's
|
1373
|
+
# search limit will be ignored!)
|
1374
|
+
# CONFIRMED: This code doesn't work on LDAPs that don't support paged searches
|
1375
|
+
# when the size limit is larger than 126. We're going to have to do a root-DSE record
|
1376
|
+
# search and not do a paged search if the LDAP doesn't support it. Yuck.
|
1377
|
+
#
|
1378
|
+
rfc2696_cookie = [126, ""]
|
1379
|
+
result_code = 0
|
1380
|
+
n_results = 0
|
1361
1381
|
|
1362
|
-
|
1363
|
-
|
1364
|
-
:mechanism => "GSS-SPNEGO",
|
1365
|
-
:initial_credential => NTLM::Message::Type1.new.serialize,
|
1366
|
-
:challenge_response => nego
|
1367
|
-
})
|
1368
|
-
end
|
1369
|
-
private :bind_gss_spnego
|
1382
|
+
loop {
|
1383
|
+
# should collect this into a private helper to clarify the structure
|
1370
1384
|
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
# which might never come. We need to support the time-limit in the protocol.
|
1379
|
-
#--
|
1380
|
-
# WARNING: this code substantially recapitulates the searchx method.
|
1381
|
-
#
|
1382
|
-
# 02May06: Well, I added support for RFC-2696-style paged searches.
|
1383
|
-
# This is used on all queries because the extension is marked non-critical.
|
1384
|
-
# As far as I know, only A/D uses this, but it's required for A/D. Otherwise
|
1385
|
-
# you won't get more than 1000 results back from a query.
|
1386
|
-
# This implementation is kindof clunky and should probably be refactored.
|
1387
|
-
# Also, is it my imagination, or are A/Ds the slowest directory servers ever???
|
1388
|
-
# OpenLDAP newer than version 2.2.0 supports paged searches.
|
1389
|
-
#
|
1390
|
-
def search args = {}
|
1391
|
-
search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
|
1392
|
-
search_filter = Filter.construct(search_filter) if search_filter.is_a?(String)
|
1393
|
-
search_base = (args && args[:base]) || "dc=example,dc=com"
|
1394
|
-
search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
|
1395
|
-
return_referrals = args && args[:return_referrals] == true
|
1396
|
-
sizelimit = (args && args[:size].to_i) || 0
|
1397
|
-
raise LdapError.new( "invalid search-size" ) unless sizelimit >= 0
|
1398
|
-
paged_searches_supported = (args && args[:paged_searches_supported])
|
1399
|
-
|
1400
|
-
attributes_only = (args and args[:attributes_only] == true)
|
1401
|
-
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
|
1402
|
-
raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
|
1403
|
-
|
1404
|
-
# An interesting value for the size limit would be close to A/D's built-in
|
1405
|
-
# page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
|
1406
|
-
# on anything bigger than 126. You get a silent error that is easily visible
|
1407
|
-
# by running slapd in debug mode. Go figure.
|
1408
|
-
#
|
1409
|
-
# Changed this around 06Sep06 to support a caller-specified search-size limit.
|
1410
|
-
# Because we ALWAYS do paged searches, we have to work around the problem that
|
1411
|
-
# it's not legal to specify a "normal" sizelimit (in the body of the search request)
|
1412
|
-
# that is larger than the page size we're requesting. Unfortunately, I have the
|
1413
|
-
# feeling that this will break with LDAP servers that don't support paged searches!!!
|
1414
|
-
# (Because we pass zero as the sizelimit on search rounds when the remaining limit
|
1415
|
-
# is larger than our max page size of 126. In these cases, I think the caller's
|
1416
|
-
# search limit will be ignored!)
|
1417
|
-
# CONFIRMED: This code doesn't work on LDAPs that don't support paged searches
|
1418
|
-
# when the size limit is larger than 126. We're going to have to do a root-DSE record
|
1419
|
-
# search and not do a paged search if the LDAP doesn't support it. Yuck.
|
1420
|
-
#
|
1421
|
-
rfc2696_cookie = [126, ""]
|
1422
|
-
result_code = 0
|
1423
|
-
n_results = 0
|
1424
|
-
|
1425
|
-
loop {
|
1426
|
-
# should collect this into a private helper to clarify the structure
|
1427
|
-
|
1428
|
-
query_limit = 0
|
1429
|
-
if sizelimit > 0
|
1430
|
-
if paged_searches_supported
|
1431
|
-
query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - n_results) : 0)
|
1432
|
-
else
|
1433
|
-
query_limit = sizelimit
|
1385
|
+
query_limit = 0
|
1386
|
+
if sizelimit > 0
|
1387
|
+
if paged_searches_supported
|
1388
|
+
query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - n_results) : 0)
|
1389
|
+
else
|
1390
|
+
query_limit = sizelimit
|
1391
|
+
end
|
1434
1392
|
end
|
1435
|
-
end
|
1436
1393
|
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
|
1445
|
-
|
1446
|
-
|
1394
|
+
request = [
|
1395
|
+
search_base.to_ber,
|
1396
|
+
scope.to_ber_enumerated,
|
1397
|
+
0.to_ber_enumerated,
|
1398
|
+
query_limit.to_ber, # size limit
|
1399
|
+
0.to_ber,
|
1400
|
+
attributes_only.to_ber,
|
1401
|
+
search_filter.to_ber,
|
1402
|
+
search_attributes.to_ber_sequence
|
1403
|
+
].to_ber_appsequence(3)
|
1447
1404
|
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1405
|
+
controls = [
|
1406
|
+
[
|
1407
|
+
LdapControls::PagedResults.to_ber,
|
1408
|
+
false.to_ber, # criticality MUST be false to interoperate with normal LDAPs.
|
1409
|
+
rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
|
1410
|
+
].to_ber_sequence
|
1411
|
+
].to_ber_contextspecific(0)
|
1412
|
+
|
1413
|
+
pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
|
1414
|
+
@conn.write pkt
|
1415
|
+
|
1416
|
+
result_code = 0
|
1417
|
+
controls = []
|
1418
|
+
|
1419
|
+
while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
|
1420
|
+
case pdu.app_tag
|
1421
|
+
when 4 # search-data
|
1422
|
+
n_results += 1
|
1423
|
+
yield( pdu.search_entry ) if block_given?
|
1424
|
+
when 19 # search-referral
|
1425
|
+
if return_referrals
|
1426
|
+
if block_given?
|
1427
|
+
se = Net::LDAP::Entry.new
|
1428
|
+
se[:search_referrals] = (pdu.search_referrals || [])
|
1429
|
+
yield se
|
1430
|
+
end
|
1473
1431
|
end
|
1432
|
+
#p pdu.referrals
|
1433
|
+
when 5 # search-result
|
1434
|
+
result_code = pdu.result_code
|
1435
|
+
controls = pdu.result_controls
|
1436
|
+
break
|
1437
|
+
else
|
1438
|
+
raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
|
1474
1439
|
end
|
1475
|
-
#p pdu.referrals
|
1476
|
-
when 5 # search-result
|
1477
|
-
result_code = pdu.result_code
|
1478
|
-
controls = pdu.result_controls
|
1479
|
-
break
|
1480
|
-
else
|
1481
|
-
raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
|
1482
1440
|
end
|
1483
|
-
end
|
1484
1441
|
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1442
|
+
# When we get here, we have seen a type-5 response.
|
1443
|
+
# If there is no error AND there is an RFC-2696 cookie,
|
1444
|
+
# then query again for the next page of results.
|
1445
|
+
# If not, we're done.
|
1446
|
+
# Don't screw this up or we'll break every search we do.
|
1447
|
+
#
|
1448
|
+
# Noticed 02Sep06, look at the read_ber call in this loop,
|
1449
|
+
# shouldn't that have a parameter of AsnSyntax? Does this
|
1450
|
+
# just accidentally work? According to RFC-2696, the value
|
1451
|
+
# expected in this position is of type OCTET STRING, covered
|
1452
|
+
# in the default syntax supported by read_ber, so I guess
|
1453
|
+
# we're ok.
|
1454
|
+
#
|
1455
|
+
more_pages = false
|
1456
|
+
if result_code == 0 and controls
|
1457
|
+
controls.each do |c|
|
1458
|
+
if c.oid == LdapControls::PagedResults
|
1459
|
+
more_pages = false # just in case some bogus server sends us >1 of these.
|
1460
|
+
if c.value and c.value.length > 0
|
1461
|
+
cookie = c.value.read_ber[1]
|
1462
|
+
if cookie and cookie.length > 0
|
1463
|
+
rfc2696_cookie[1] = cookie
|
1464
|
+
more_pages = true
|
1465
|
+
end
|
1508
1466
|
end
|
1509
1467
|
end
|
1510
1468
|
end
|
1511
1469
|
end
|
1512
|
-
end
|
1513
1470
|
|
1514
|
-
|
1515
|
-
|
1471
|
+
break unless more_pages
|
1472
|
+
} # loop
|
1516
1473
|
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
#--
|
1523
|
-
# modify
|
1524
|
-
# TODO, need to support a time limit, in case the server fails to respond.
|
1525
|
-
# TODO!!! We're throwing an exception here on empty DN.
|
1526
|
-
# Should return a proper error instead, probaby from farther up the chain.
|
1527
|
-
# TODO!!! If the user specifies a bogus opcode, we'll throw a
|
1528
|
-
# confusing error here ("to_ber_enumerated is not defined on nil").
|
1529
|
-
#
|
1530
|
-
def modify args
|
1531
|
-
modify_dn = args[:dn] or raise "Unable to modify empty DN"
|
1532
|
-
modify_ops = []
|
1533
|
-
a = args[:operations] and a.each {|op, attr, values|
|
1534
|
-
# TODO, fix the following line, which gives a bogus error
|
1535
|
-
# if the opcode is invalid.
|
1536
|
-
op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
|
1537
|
-
modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
|
1538
|
-
}
|
1539
|
-
|
1540
|
-
request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
|
1541
|
-
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
1542
|
-
@conn.write pkt
|
1543
|
-
|
1544
|
-
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
|
1545
|
-
pdu.result
|
1546
|
-
end
|
1547
|
-
|
1548
|
-
|
1549
|
-
#--
|
1550
|
-
# add
|
1551
|
-
# TODO, need to support a time limit, in case the server fails to respond.
|
1552
|
-
# Unlike other operation-methods in this class, we return a result hash rather
|
1553
|
-
# than a simple result number. This is experimental, and eventually we'll want
|
1554
|
-
# to do this with all the others. The point is to have access to the error message
|
1555
|
-
# and the matched-DN returned by the server.
|
1556
|
-
#
|
1557
|
-
def add args
|
1558
|
-
add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
|
1559
|
-
add_attrs = []
|
1560
|
-
a = args[:attributes] and a.each {|k,v|
|
1561
|
-
add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
|
1562
|
-
}
|
1563
|
-
|
1564
|
-
request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
|
1565
|
-
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
1566
|
-
@conn.write pkt
|
1474
|
+
result_code
|
1475
|
+
end
|
1567
1476
|
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1477
|
+
#--
|
1478
|
+
# modify
|
1479
|
+
# TODO, need to support a time limit, in case the server fails to respond.
|
1480
|
+
# TODO!!! We're throwing an exception here on empty DN.
|
1481
|
+
# Should return a proper error instead, probaby from farther up the chain.
|
1482
|
+
# TODO!!! If the user specifies a bogus opcode, we'll throw a
|
1483
|
+
# confusing error here ("to_ber_enumerated is not defined on nil").
|
1484
|
+
#++
|
1485
|
+
def modify args
|
1486
|
+
modify_dn = args[:dn] or raise "Unable to modify empty DN"
|
1487
|
+
modify_ops = []
|
1488
|
+
a = args[:operations] and a.each {|op, attr, values|
|
1489
|
+
# TODO, fix the following line, which gives a bogus error
|
1490
|
+
# if the opcode is invalid.
|
1491
|
+
op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
|
1492
|
+
modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
|
1493
|
+
}
|
1571
1494
|
|
1495
|
+
request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
|
1496
|
+
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
1497
|
+
@conn.write pkt
|
1572
1498
|
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
#
|
1577
|
-
def rename args
|
1578
|
-
old_dn = args[:olddn] or raise "Unable to rename empty DN"
|
1579
|
-
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
|
1580
|
-
delete_attrs = args[:delete_attributes] ? true : false
|
1499
|
+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
|
1500
|
+
pdu.result
|
1501
|
+
end
|
1581
1502
|
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1503
|
+
#--
|
1504
|
+
# add
|
1505
|
+
# TODO, need to support a time limit, in case the server fails to respond.
|
1506
|
+
# Unlike other operation-methods in this class, we return a result hash rather
|
1507
|
+
# than a simple result number. This is experimental, and eventually we'll want
|
1508
|
+
# to do this with all the others. The point is to have access to the error message
|
1509
|
+
# and the matched-DN returned by the server.
|
1510
|
+
#++
|
1511
|
+
def add args
|
1512
|
+
add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
|
1513
|
+
add_attrs = []
|
1514
|
+
a = args[:attributes] and a.each {|k,v|
|
1515
|
+
add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
|
1516
|
+
}
|
1585
1517
|
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1518
|
+
request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
|
1519
|
+
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
1520
|
+
@conn.write pkt
|
1589
1521
|
|
1522
|
+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
|
1523
|
+
pdu.result
|
1524
|
+
end
|
1590
1525
|
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1526
|
+
#--
|
1527
|
+
# rename
|
1528
|
+
# TODO, need to support a time limit, in case the server fails to respond.
|
1529
|
+
#++
|
1530
|
+
def rename args
|
1531
|
+
old_dn = args[:olddn] or raise "Unable to rename empty DN"
|
1532
|
+
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
|
1533
|
+
delete_attrs = args[:delete_attributes] ? true : false
|
1534
|
+
|
1535
|
+
request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
|
1536
|
+
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
1537
|
+
@conn.write pkt
|
1597
1538
|
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1539
|
+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
|
1540
|
+
pdu.result_code
|
1541
|
+
end
|
1601
1542
|
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1543
|
+
#--
|
1544
|
+
# delete
|
1545
|
+
# TODO, need to support a time limit, in case the server fails to respond.
|
1546
|
+
#++
|
1547
|
+
def delete args
|
1548
|
+
dn = args[:dn] or raise "Unable to delete empty DN"
|
1605
1549
|
|
1550
|
+
request = dn.to_s.to_ber_application_string(10)
|
1551
|
+
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
1552
|
+
@conn.write pkt
|
1606
1553
|
|
1607
|
-
|
1554
|
+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" )
|
1555
|
+
pdu.result_code
|
1556
|
+
end
|
1557
|
+
end # class Connection
|
1608
1558
|
end # class LDAP
|
1609
|
-
|
1610
|
-
|
1611
1559
|
end # module Net
|
1612
|
-
|
1613
|
-
|