net-ldap 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
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
|
-
|