scashin133-net-ldap 0.1.2

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.
data/lib/net/ldap.rb ADDED
@@ -0,0 +1,1536 @@
1
+ require 'ostruct'
2
+
3
+ module Net
4
+ class LDAP
5
+ begin
6
+ require 'openssl'
7
+ HasOpenSSL = true
8
+ rescue LoadError
9
+ HasOpenSSL = false
10
+ end
11
+ end
12
+ end
13
+ require 'socket'
14
+
15
+ require 'net/ber'
16
+ require 'net/ldap/pdu'
17
+ require 'net/ldap/filter'
18
+ require 'net/ldap/dataset'
19
+ require 'net/ldap/password'
20
+ require 'net/ldap/entry'
21
+
22
+ # == Net::LDAP
23
+ #
24
+ # This library provides a pure-Ruby implementation of the LDAP client
25
+ # protocol, per RFC-2251. It can be used to access any server which
26
+ # implements the LDAP protocol.
27
+ #
28
+ # Net::LDAP is intended to provide full LDAP functionality while hiding the
29
+ # more arcane aspects the LDAP protocol itself, and thus presenting as
30
+ # Ruby-like a programming interface as possible.
31
+ #
32
+ # == Quick-start for the Impatient
33
+ # === Quick Example of a user-authentication against an LDAP directory:
34
+ #
35
+ # require 'rubygems'
36
+ # require 'net/ldap'
37
+ #
38
+ # ldap = Net::LDAP.new
39
+ # ldap.host = your_server_ip_address
40
+ # ldap.port = 389
41
+ # ldap.auth "joe_user", "opensesame"
42
+ # if ldap.bind
43
+ # # authentication succeeded
44
+ # else
45
+ # # authentication failed
46
+ # end
47
+ #
48
+ #
49
+ # === Quick Example of a search against an LDAP directory:
50
+ #
51
+ # require 'rubygems'
52
+ # require 'net/ldap'
53
+ #
54
+ # ldap = Net::LDAP.new :host => server_ip_address,
55
+ # :port => 389,
56
+ # :auth => {
57
+ # :method => :simple,
58
+ # :username => "cn=manager, dc=example, dc=com",
59
+ # :password => "opensesame"
60
+ # }
61
+ #
62
+ # filter = Net::LDAP::Filter.eq("cn", "George*")
63
+ # treebase = "dc=example, dc=com"
64
+ #
65
+ # ldap.search(:base => treebase, :filter => filter) do |entry|
66
+ # puts "DN: #{entry.dn}"
67
+ # entry.each do |attribute, values|
68
+ # puts " #{attribute}:"
69
+ # values.each do |value|
70
+ # puts " --->#{value}"
71
+ # end
72
+ # end
73
+ # end
74
+ #
75
+ # p ldap.get_operation_result
76
+ #
77
+ #
78
+ # == A Brief Introduction to LDAP
79
+ #
80
+ # We're going to provide a quick, informal introduction to LDAP terminology
81
+ # and typical operations. If you're comfortable with this material, skip
82
+ # ahead to "How to use Net::LDAP." If you want a more rigorous treatment of
83
+ # this material, we recommend you start with the various IETF and ITU
84
+ # standards that relate to LDAP.
85
+ #
86
+ # === Entities
87
+ # LDAP is an Internet-standard protocol used to access directory servers.
88
+ # The basic search unit is the <i>entity, </i> which corresponds to a person
89
+ # or other domain-specific object. A directory service which supports the
90
+ # LDAP protocol typically stores information about a number of entities.
91
+ #
92
+ # === Principals
93
+ # LDAP servers are typically used to access information about people, but
94
+ # also very often about such items as printers, computers, and other
95
+ # resources. To reflect this, LDAP uses the term <i>entity, </i> or less
96
+ # commonly, <i>principal, </i> to denote its basic data-storage unit.
97
+ #
98
+ # === Distinguished Names
99
+ # In LDAP's view of the world, an entity is uniquely identified by a
100
+ # globally-unique text string called a <i>Distinguished Name, </i> originally
101
+ # defined in the X.400 standards from which LDAP is ultimately derived. Much
102
+ # like a DNS hostname, a DN is a "flattened" text representation of a string
103
+ # of tree nodes. Also like DNS (and unlike Java package names), a DN
104
+ # expresses a chain of tree-nodes written from left to right in order from
105
+ # the most-resolved node to the most-general one.
106
+ #
107
+ # If you know the DN of a person or other entity, then you can query an
108
+ # LDAP-enabled directory for information (attributes) about the entity.
109
+ # Alternatively, you can query the directory for a list of DNs matching a
110
+ # set of criteria that you supply.
111
+ #
112
+ # === Attributes
113
+ #
114
+ # In the LDAP view of the world, a DN uniquely identifies an entity.
115
+ # Information about the entity is stored as a set of <i>Attributes.</i> An
116
+ # attribute is a text string which is associated with zero or more values.
117
+ # Most LDAP-enabled directories store a well-standardized range of
118
+ # attributes, and constrain their values according to standard rules.
119
+ #
120
+ # A good example of an attribute is <tt>sn, </tt> which stands for "Surname."
121
+ # This attribute is generally used to store a person's surname, or last
122
+ # name. Most directories enforce the standard convention that an entity's
123
+ # <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP jargon, that
124
+ # means that <tt>sn</tt> must be <i>present</i> and <i>single-valued.</i>
125
+ #
126
+ # Another attribute is <tt>mail, </tt> which is used to store email
127
+ # addresses. (No, there is no attribute called "email, " perhaps because
128
+ # X.400 terminology predates the invention of the term <i>email.</i>)
129
+ # <tt>mail</tt> differs from <tt>sn</tt> in that most directories permit any
130
+ # number of values for the <tt>mail</tt> attribute, including zero.
131
+ #
132
+ # === Tree-Base
133
+ # We said above that X.400 Distinguished Names are <i>globally unique.</i>
134
+ # In a manner reminiscent of DNS, LDAP supposes that each directory server
135
+ # contains authoritative attribute data for a set of DNs corresponding to a
136
+ # specific sub-tree of the (notional) global directory tree. This subtree is
137
+ # generally configured into a directory server when it is created. It
138
+ # matters for this discussion because most servers will not allow you to
139
+ # query them unless you specify a correct tree-base.
140
+ #
141
+ # Let's say you work for the engineering department of Big Company, Inc.,
142
+ # whose internet domain is bigcompany.com. You may find that your
143
+ # departmental directory is stored in a server with a defined tree-base of
144
+ # ou=engineering, dc=bigcompany, dc=com
145
+ # You will need to supply this string as the <i>tree-base</i> when querying
146
+ # this directory. (Ou is a very old X.400 term meaning "organizational
147
+ # unit." Dc is a more recent term meaning "domain component.")
148
+ #
149
+ # === LDAP Versions
150
+ # (stub, discuss v2 and v3)
151
+ #
152
+ # === LDAP Operations
153
+ # The essential operations are: #bind, #search, #add, #modify, #delete, and
154
+ # #rename.
155
+ #
156
+ # ==== Bind
157
+ # #bind supplies a user's authentication credentials to a server, which in
158
+ # turn verifies or rejects them. There is a range of possibilities for
159
+ # credentials, but most directories support a simple username and password
160
+ # authentication.
161
+ #
162
+ # Taken by itself, #bind can be used to authenticate a user against
163
+ # information stored in a directory, for example to permit or deny access to
164
+ # some other resource. In terms of the other LDAP operations, most
165
+ # directories require a successful #bind to be performed before the other
166
+ # operations will be permitted. Some servers permit certain operations to be
167
+ # performed with an "anonymous" binding, meaning that no credentials are
168
+ # presented by the user. (We're glossing over a lot of platform-specific
169
+ # detail here.)
170
+ #
171
+ # ==== Search
172
+ # Calling #search against the directory involves specifying a treebase, a
173
+ # set of <i>search filters, </i> and a list of attribute values. The filters
174
+ # specify ranges of possible values for particular attributes. Multiple
175
+ # filters can be joined together with AND, OR, and NOT operators. A server
176
+ # will respond to a #search by returning a list of matching DNs together
177
+ # with a set of attribute values for each entity, depending on what
178
+ # attributes the search requested.
179
+ #
180
+ # ==== Add
181
+ # #add specifies a new DN and an initial set of attribute values. If the
182
+ # operation succeeds, a new entity with the corresponding DN and attributes
183
+ # is added to the directory.
184
+ #
185
+ # ==== Modify
186
+ # #modify specifies an entity DN, and a list of attribute operations.
187
+ # #modify is used to change the attribute values stored in the directory for
188
+ # a particular entity. #modify may add or delete attributes (which are lists
189
+ # of values) or it change attributes by adding to or deleting from their
190
+ # values. Net::LDAP provides three easier methods to modify an entry's
191
+ # attribute values: #add_attribute, #replace_attribute, and
192
+ # #delete_attribute.
193
+ #
194
+ # ==== Delete
195
+ # #delete specifies an entity DN. If it succeeds, the entity and all its
196
+ # attributes is removed from the directory.
197
+ #
198
+ # ==== Rename (or Modify RDN)
199
+ # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP
200
+ # protocol. It responds to the often-arising need to change the DN of an
201
+ # entity without discarding its attribute values. In earlier LDAP versions,
202
+ # the only way to do this was to delete the whole entity and add it again
203
+ # with a different DN.
204
+ #
205
+ # #rename works by taking an "old" DN (the one to change) and a "new RDN, "
206
+ # which is the left-most part of the DN string. If successful, #rename
207
+ # changes the entity DN so that its left-most node corresponds to the new
208
+ # RDN given in the request. (RDN, or "relative distinguished name, " denotes
209
+ # a single tree-node as expressed in a DN, which is a chain of tree nodes.)
210
+ #
211
+ # == How to use Net::LDAP
212
+ # To access Net::LDAP functionality in your Ruby programs, start by
213
+ # requiring the library:
214
+ #
215
+ # require 'net/ldap'
216
+ #
217
+ # If you installed the Gem version of Net::LDAP, and depending on your
218
+ # version of Ruby and rubygems, you _may_ also need to require rubygems
219
+ # explicitly:
220
+ #
221
+ # require 'rubygems'
222
+ # require 'net/ldap'
223
+ #
224
+ # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
225
+ # The constructor for this object takes arguments specifying the network
226
+ # location (address and port) of the LDAP server, and also the binding
227
+ # (authentication) credentials, typically a username and password. Given an
228
+ # object of class Net:LDAP, you can then perform LDAP operations by calling
229
+ # instance methods on the object. These are documented with usage examples
230
+ # below.
231
+ #
232
+ # The Net::LDAP library is designed to be very disciplined about how it
233
+ # makes network connections to servers. This is different from many of the
234
+ # standard native-code libraries that are provided on most platforms, which
235
+ # share bloodlines with the original Netscape/Michigan LDAP client
236
+ # implementations. These libraries sought to insulate user code from the
237
+ # workings of the network. This is a good idea of course, but the practical
238
+ # effect has been confusing and many difficult bugs have been caused by the
239
+ # opacity of the native libraries, and their variable behavior across
240
+ # platforms.
241
+ #
242
+ # In general, Net::LDAP instance methods which invoke server operations make
243
+ # a connection to the server when the method is called. They execute the
244
+ # operation (typically binding first) and then disconnect from the server.
245
+ # The exception is Net::LDAP#open, which makes a connection to the server
246
+ # and then keeps it open while it executes a user-supplied block.
247
+ # Net::LDAP#open closes the connection on completion of the block.
248
+ class Net::LDAP
249
+ VERSION = "0.1.2"
250
+
251
+ class LdapError < StandardError; end
252
+
253
+ SearchScope_BaseObject = 0
254
+ SearchScope_SingleLevel = 1
255
+ SearchScope_WholeSubtree = 2
256
+ SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel,
257
+ SearchScope_WholeSubtree ]
258
+
259
+ primitive = { 2 => :null } # UnbindRequest body
260
+ constructed = {
261
+ 0 => :array, # BindRequest
262
+ 1 => :array, # BindResponse
263
+ 2 => :array, # UnbindRequest
264
+ 3 => :array, # SearchRequest
265
+ 4 => :array, # SearchData
266
+ 5 => :array, # SearchResult
267
+ 6 => :array, # ModifyRequest
268
+ 7 => :array, # ModifyResponse
269
+ 8 => :array, # AddRequest
270
+ 9 => :array, # AddResponse
271
+ 10 => :array, # DelRequest
272
+ 11 => :array, # DelResponse
273
+ 12 => :array, # ModifyRdnRequest
274
+ 13 => :array, # ModifyRdnResponse
275
+ 14 => :array, # CompareRequest
276
+ 15 => :array, # CompareResponse
277
+ 16 => :array, # AbandonRequest
278
+ 19 => :array, # SearchResultReferral
279
+ 24 => :array, # Unsolicited Notification
280
+ }
281
+ application = {
282
+ :primitive => primitive,
283
+ :constructed => constructed,
284
+ }
285
+ primitive = {
286
+ 0 => :string, # password
287
+ 1 => :string, # Kerberos v4
288
+ 2 => :string, # Kerberos v5
289
+ 3 => :string, # SearchFilter-extensible
290
+ 4 => :string, # SearchFilter-extensible
291
+ 7 => :string, # serverSaslCreds
292
+ }
293
+ constructed = {
294
+ 0 => :array, # RFC-2251 Control and Filter-AND
295
+ 1 => :array, # SearchFilter-OR
296
+ 2 => :array, # SearchFilter-NOT
297
+ 3 => :array, # Seach referral
298
+ 4 => :array, # unknown use in Microsoft Outlook
299
+ 5 => :array, # SearchFilter-GE
300
+ 6 => :array, # SearchFilter-LE
301
+ 7 => :array, # serverSaslCreds
302
+ 9 => :array, # SearchFilter-extensible
303
+ }
304
+ context_specific = {
305
+ :primitive => primitive,
306
+ :constructed => constructed,
307
+ }
308
+
309
+ AsnSyntax = Net::BER.compile_syntax(:application => application,
310
+ :context_specific => context_specific)
311
+
312
+ DefaultHost = "127.0.0.1"
313
+ DefaultPort = 389
314
+ DefaultAuth = { :method => :anonymous }
315
+ DefaultTreebase = "dc=com"
316
+
317
+ StartTlsOid = "1.3.6.1.4.1.1466.20037"
318
+
319
+ ResultStrings = {
320
+ 0 => "Success",
321
+ 1 => "Operations Error",
322
+ 2 => "Protocol Error",
323
+ 3 => "Time Limit Exceeded",
324
+ 4 => "Size Limit Exceeded",
325
+ 12 => "Unavailable crtical extension",
326
+ 14 => "saslBindInProgress",
327
+ 16 => "No Such Attribute",
328
+ 17 => "Undefined Attribute Type",
329
+ 20 => "Attribute or Value Exists",
330
+ 32 => "No Such Object",
331
+ 34 => "Invalid DN Syntax",
332
+ 48 => "Inappropriate Authentication",
333
+ 49 => "Invalid Credentials",
334
+ 50 => "Insufficient Access Rights",
335
+ 51 => "Busy",
336
+ 52 => "Unavailable",
337
+ 53 => "Unwilling to perform",
338
+ 65 => "Object Class Violation",
339
+ 68 => "Entry Already Exists"
340
+ }
341
+
342
+ module LdapControls
343
+ PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
344
+ end
345
+
346
+ def self.result2string(code) #:nodoc:
347
+ ResultStrings[code] || "unknown result (#{code})"
348
+ end
349
+
350
+ attr_accessor :host
351
+ attr_accessor :port
352
+ attr_accessor :base
353
+
354
+ # Instantiate an object of type Net::LDAP to perform directory operations.
355
+ # This constructor takes a Hash containing arguments, all of which are
356
+ # either optional or may be specified later with other methods as
357
+ # described below. The following arguments are supported:
358
+ # * :host => the LDAP server's IP-address (default 127.0.0.1)
359
+ # * :port => the LDAP server's TCP port (default 389)
360
+ # * :auth => a Hash containing authorization parameters. Currently
361
+ # supported values include: {:method => :anonymous} and {:method =>
362
+ # :simple, :username => your_user_name, :password => your_password }
363
+ # The password parameter may be a Proc that returns a String.
364
+ # * :base => a default treebase parameter for searches performed against
365
+ # the LDAP server. If you don't give this value, then each call to
366
+ # #search must specify a treebase parameter. If you do give this value,
367
+ # then it will be used in subsequent calls to #search that do not
368
+ # specify a treebase. If you give a treebase value in any particular
369
+ # call to #search, that value will override any treebase value you give
370
+ # here.
371
+ # * :encryption => specifies the encryption to be used in communicating
372
+ # with the LDAP server. The value is either a Hash containing additional
373
+ # parameters, or the Symbol :simple_tls, which is equivalent to
374
+ # specifying the Hash {:method => :simple_tls}. There is a fairly large
375
+ # range of potential values that may be given for this parameter. See
376
+ # #encryption for details.
377
+ #
378
+ # Instantiating a Net::LDAP object does <i>not</i> result in network
379
+ # traffic to the LDAP server. It simply stores the connection and binding
380
+ # parameters in the object.
381
+ def initialize(args = {})
382
+ @host = args[:host] || DefaultHost
383
+ @port = args[:port] || DefaultPort
384
+ @verbose = false # Make this configurable with a switch on the class.
385
+ @auth = args[:auth] || DefaultAuth
386
+ @base = args[:base] || DefaultTreebase
387
+ encryption args[:encryption] # may be nil
388
+
389
+ if pr = @auth[:password] and pr.respond_to?(:call)
390
+ @auth[:password] = pr.call
391
+ end
392
+
393
+ # This variable is only set when we are created with LDAP::open. All of
394
+ # our internal methods will connect using it, or else they will create
395
+ # their own.
396
+ @open_connection = nil
397
+ end
398
+
399
+ # Convenience method to specify authentication credentials to the LDAP
400
+ # server. Currently supports simple authentication requiring a username
401
+ # and password.
402
+ #
403
+ # Observe that on most LDAP servers, the username is a complete DN.
404
+ # However, with A/D, it's often possible to give only a user-name rather
405
+ # than a complete DN. In the latter case, beware that many A/D servers are
406
+ # configured to permit anonymous (uncredentialled) binding, and will
407
+ # silently accept your binding as anonymous if you give an unrecognized
408
+ # username. This is not usually what you want. (See
409
+ # #get_operation_result.)
410
+ #
411
+ # <b>Important:</b> The password argument may be a Proc that returns a
412
+ # string. This makes it possible for you to write client programs that
413
+ # solicit passwords from users or from other data sources without showing
414
+ # them in your code or on command lines.
415
+ #
416
+ # require 'net/ldap'
417
+ #
418
+ # ldap = Net::LDAP.new
419
+ # ldap.host = server_ip_address
420
+ # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", "your_psw"
421
+ #
422
+ # Alternatively (with a password block):
423
+ #
424
+ # require 'net/ldap'
425
+ #
426
+ # ldap = Net::LDAP.new
427
+ # ldap.host = server_ip_address
428
+ # psw = proc { your_psw_function }
429
+ # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", psw
430
+ #
431
+ def authenticate(username, password)
432
+ password = password.call if password.respond_to?(:call)
433
+ @auth = {
434
+ :method => :simple,
435
+ :username => username,
436
+ :password => password
437
+ }
438
+ end
439
+ alias_method :auth, :authenticate
440
+
441
+ # Convenience method to specify encryption characteristics for connections
442
+ # to LDAP servers. Called implicitly by #new and #open, but may also be
443
+ # called by user code if desired. The single argument is generally a Hash
444
+ # (but see below for convenience alternatives). This implementation is
445
+ # currently a stub, supporting only a few encryption alternatives. As
446
+ # additional capabilities are added, more configuration values will be
447
+ # added here.
448
+ #
449
+ # Currently, the only supported argument is { :method => :simple_tls }.
450
+ # (Equivalently, you may pass the symbol :simple_tls all by itself,
451
+ # without enclosing it in a Hash.)
452
+ #
453
+ # The :simple_tls encryption method encrypts <i>all</i> communications
454
+ # with the LDAP server. It completely establishes SSL/TLS encryption with
455
+ # the LDAP server before any LDAP-protocol data is exchanged. There is no
456
+ # plaintext negotiation and no special encryption-request controls are
457
+ # sent to the server. <i>The :simple_tls option is the simplest, easiest
458
+ # way to encrypt communications between Net::LDAP and LDAP servers.</i>
459
+ # It's intended for cases where you have an implicit level of trust in the
460
+ # authenticity of the LDAP server. No validation of the LDAP server's SSL
461
+ # certificate is performed. This means that :simple_tls will not produce
462
+ # errors if the LDAP server's encryption certificate is not signed by a
463
+ # well-known Certification Authority. If you get communications or
464
+ # protocol errors when using this option, check with your LDAP server
465
+ # administrator. Pay particular attention to the TCP port you are
466
+ # connecting to. It's impossible for an LDAP server to support plaintext
467
+ # LDAP communications and <i>simple TLS</i> connections on the same port.
468
+ # The standard TCP port for unencrypted LDAP connections is 389, but the
469
+ # standard port for simple-TLS encrypted connections is 636. Be sure you
470
+ # are using the correct port.
471
+ #
472
+ # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP
473
+ # control, which will enable encrypted communications on the same TCP port
474
+ # used for unencrypted connections.]</i>
475
+ def encryption(args)
476
+ case args
477
+ when :simple_tls, :start_tls
478
+ args = { :method => args }
479
+ end
480
+ @encryption = args
481
+ end
482
+
483
+ # #open takes the same parameters as #new. #open makes a network
484
+ # connection to the LDAP server and then passes a newly-created Net::LDAP
485
+ # object to the caller-supplied block. Within the block, you can call any
486
+ # of the instance methods of Net::LDAP to perform operations against the
487
+ # LDAP directory. #open will perform all the operations in the
488
+ # user-supplied block on the same network connection, which will be closed
489
+ # automatically when the block finishes.
490
+ #
491
+ # # (PSEUDOCODE)
492
+ # auth = { :method => :simple, :username => username, :password => password }
493
+ # Net::LDAP.open(:host => ipaddress, :port => 389, :auth => auth) do |ldap|
494
+ # ldap.search(...)
495
+ # ldap.add(...)
496
+ # ldap.modify(...)
497
+ # end
498
+ def self.open(args)
499
+ ldap1 = LDAP.new(args)
500
+ ldap1.open { |ldap| yield ldap }
501
+ end
502
+
503
+ # Returns a meaningful result any time after a protocol operation (#bind,
504
+ # #search, #add, #modify, #rename, #delete) has completed. It returns an
505
+ # #OpenStruct containing an LDAP result code (0 means success), and a
506
+ # human-readable string.
507
+ #
508
+ # unless ldap.bind
509
+ # puts "Result: #{ldap.get_operation_result.code}"
510
+ # puts "Message: #{ldap.get_operation_result.message}"
511
+ # end
512
+ #
513
+ # Certain operations return additional information, accessible through
514
+ # members of the object returned from #get_operation_result. Check
515
+ # #get_operation_result.error_message and
516
+ # #get_operation_result.matched_dn.
517
+ #
518
+ #--
519
+ # Modified the implementation, 20Mar07. We might get a hash of LDAP
520
+ # response codes instead of a simple numeric code.
521
+ #++
522
+ def get_operation_result
523
+ os = OpenStruct.new
524
+ if @result.is_a?(Hash)
525
+ # We might get a hash of LDAP response codes instead of a simple
526
+ # numeric code.
527
+ os.code = (@result[:resultCode] || "").to_i
528
+ os.error_message = @result[:errorMessage]
529
+ os.matched_dn = @result[:matchedDN]
530
+ elsif @result
531
+ os.code = @result
532
+ else
533
+ os.code = 0
534
+ end
535
+ os.message = LDAP.result2string(os.code)
536
+ os
537
+ end
538
+
539
+ # Opens a network connection to the server and then passes <tt>self</tt>
540
+ # to the caller-supplied block. The connection is closed when the block
541
+ # completes. Used for executing multiple LDAP operations without requiring
542
+ # a separate network connection (and authentication) for each one.
543
+ # <i>Note:</i> You do not need to log-in or "bind" to the server. This
544
+ # will be done for you automatically. For an even simpler approach, see
545
+ # the class method Net::LDAP#open.
546
+ #
547
+ # # (PSEUDOCODE)
548
+ # auth = { :method => :simple, :username => username, :password => password }
549
+ # ldap = Net::LDAP.new(:host => ipaddress, :port => 389, :auth => auth)
550
+ # ldap.open do |ldap|
551
+ # ldap.search(...)
552
+ # ldap.add(...)
553
+ # ldap.modify(...)
554
+ # end
555
+ def open
556
+ # First we make a connection and then a binding, but we don't do
557
+ # anything with the bind results. We then pass self to the caller's
558
+ # block, where he will execute his LDAP operations. Of course they will
559
+ # all generate auth failures if the bind was unsuccessful.
560
+ raise Net::LDAP::LdapError, "Open already in progress" if @open_connection
561
+
562
+ begin
563
+ @open_connection = Net::LDAP::Connection.new(:host => @host,
564
+ :port => @port,
565
+ :encryption =>
566
+ @encryption)
567
+ @open_connection.bind(@auth)
568
+ yield self
569
+ ensure
570
+ @open_connection.close if @open_connection
571
+ @open_connection = nil
572
+ end
573
+ end
574
+
575
+ # Searches the LDAP directory for directory entries. Takes a hash argument
576
+ # with parameters. Supported parameters include:
577
+ # * :base (a string specifying the tree-base for the search);
578
+ # * :filter (an object of type Net::LDAP::Filter, defaults to
579
+ # objectclass=*);
580
+ # * :attributes (a string or array of strings specifying the LDAP
581
+ # attributes to return from the server);
582
+ # * :return_result (a boolean specifying whether to return a result set).
583
+ # * :attributes_only (a boolean flag, defaults false)
584
+ # * :scope (one of: Net::LDAP::SearchScope_BaseObject,
585
+ # Net::LDAP::SearchScope_SingleLevel,
586
+ # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
587
+ # * :size (an integer indicating the maximum number of search entries to
588
+ # return. Default is zero, which signifies no limit.)
589
+ #
590
+ # #search queries the LDAP server and passes <i>each entry</i> to the
591
+ # caller-supplied block, as an object of type Net::LDAP::Entry. If the
592
+ # search returns 1000 entries, the block will be called 1000 times. If the
593
+ # search returns no entries, the block will not be called.
594
+ #
595
+ # #search returns either a result-set or a boolean, depending on the value
596
+ # of the <tt>:return_result</tt> argument. The default behavior is to
597
+ # return a result set, which is an Array of objects of class
598
+ # Net::LDAP::Entry. If you request a result set and #search fails with an
599
+ # error, it will return nil. Call #get_operation_result to get the error
600
+ # information returned by
601
+ # the LDAP server.
602
+ #
603
+ # When <tt>:return_result => false, </tt> #search will return only a
604
+ # Boolean, to indicate whether the operation succeeded. This can improve
605
+ # performance with very large result sets, because the library can discard
606
+ # each entry from memory after your block processes it.
607
+ #
608
+ # treebase = "dc=example, dc=com"
609
+ # filter = Net::LDAP::Filter.eq("mail", "a*.com")
610
+ # attrs = ["mail", "cn", "sn", "objectclass"]
611
+ # ldap.search(:base => treebase, :filter => filter, :attributes => attrs,
612
+ # :return_result => false) do |entry|
613
+ # puts "DN: #{entry.dn}"
614
+ # entry.each do |attr, values|
615
+ # puts ".......#{attr}:"
616
+ # values.each do |value|
617
+ # puts " #{value}"
618
+ # end
619
+ # end
620
+ # end
621
+ def search(args = {})
622
+ unless args[:ignore_server_caps]
623
+ args[:paged_searches_supported] = paged_searches_supported?
624
+ end
625
+
626
+ args[:base] ||= @base
627
+ result_set = (args and args[:return_result] == false) ? nil : []
628
+
629
+ if @open_connection
630
+ @result = @open_connection.search(args) { |entry|
631
+ result_set << entry if result_set
632
+ yield entry if block_given?
633
+ }
634
+ else
635
+ @result = 0
636
+ begin
637
+ conn = Net::LDAP::Connection.new(:host => @host, :port => @port,
638
+ :encryption => @encryption)
639
+ if (@result = conn.bind(args[:auth] || @auth)) == 0
640
+ @result = conn.search(args) { |entry|
641
+ result_set << entry if result_set
642
+ yield entry if block_given?
643
+ }
644
+ end
645
+ ensure
646
+ conn.close if conn
647
+ end
648
+ end
649
+
650
+ @result == 0 and result_set
651
+ end
652
+
653
+ # #bind connects to an LDAP server and requests authentication based on
654
+ # the <tt>:auth</tt> parameter passed to #open or #new. It takes no
655
+ # parameters.
656
+ #
657
+ # User code does not need to call #bind directly. It will be called
658
+ # implicitly by the library whenever you invoke an LDAP operation, such as
659
+ # #search or #add.
660
+ #
661
+ # It is useful, however, to call #bind in your own code when the only
662
+ # operation you intend to perform against the directory is to validate a
663
+ # login credential. #bind returns true or false to indicate whether the
664
+ # binding was successful. Reasons for failure include malformed or
665
+ # unrecognized usernames and incorrect passwords. Use
666
+ # #get_operation_result to find out what happened in case of failure.
667
+ #
668
+ # Here's a typical example using #bind to authenticate a credential which
669
+ # was (perhaps) solicited from the user of a web site:
670
+ #
671
+ # require 'net/ldap'
672
+ # ldap = Net::LDAP.new
673
+ # ldap.host = your_server_ip_address
674
+ # ldap.port = 389
675
+ # ldap.auth your_user_name, your_user_password
676
+ # if ldap.bind
677
+ # # authentication succeeded
678
+ # else
679
+ # # authentication failed
680
+ # p ldap.get_operation_result
681
+ # end
682
+ #
683
+ # Here's a more succinct example which does exactly the same thing, but
684
+ # collects all the required parameters into arguments:
685
+ #
686
+ # require 'net/ldap'
687
+ # ldap = Net::LDAP.new(:host => your_server_ip_address, :port => 389)
688
+ # if ldap.bind(:method => :simple, :username => your_user_name,
689
+ # :password => your_user_password)
690
+ # # authentication succeeded
691
+ # else
692
+ # # authentication failed
693
+ # p ldap.get_operation_result
694
+ # end
695
+ #
696
+ # You don't need to pass a user-password as a String object to bind. You
697
+ # can also pass a Ruby Proc object which returns a string. This will cause
698
+ # bind to execute the Proc (which might then solicit input from a user
699
+ # with console display suppressed). The String value returned from the
700
+ # Proc is used as the password.
701
+ #
702
+ # You don't have to create a new instance of Net::LDAP every time you
703
+ # perform a binding in this way. If you prefer, you can cache the
704
+ # Net::LDAP object and re-use it to perform subsequent bindings,
705
+ # <i>provided</i> you call #auth to specify a new credential before
706
+ # calling #bind. Otherwise, you'll just re-authenticate the previous user!
707
+ # (You don't need to re-set the values of #host and #port.) As noted in
708
+ # the documentation for #auth, the password parameter can be a Ruby Proc
709
+ # instead of a String.
710
+ def bind(auth = @auth)
711
+ if @open_connection
712
+ @result = @open_connection.bind(auth)
713
+ else
714
+ begin
715
+ conn = Connection.new(:host => @host, :port => @port,
716
+ :encryption => @encryption)
717
+ @result = conn.bind(auth)
718
+ ensure
719
+ conn.close if conn
720
+ end
721
+ end
722
+
723
+ @result == 0
724
+ end
725
+
726
+ # #bind_as is for testing authentication credentials.
727
+ #
728
+ # As described under #bind, most LDAP servers require that you supply a
729
+ # complete DN as a binding-credential, along with an authenticator such as
730
+ # a password. But for many applications (such as authenticating users to a
731
+ # Rails application), you often don't have a full DN to identify the user.
732
+ # You usually get a simple identifier like a username or an email address,
733
+ # along with a password. #bind_as allows you to authenticate these
734
+ # user-identifiers.
735
+ #
736
+ # #bind_as is a combination of a search and an LDAP binding. First, it
737
+ # connects and binds to the directory as normal. Then it searches the
738
+ # directory for an entry corresponding to the email address, username, or
739
+ # other string that you supply. If the entry exists, then #bind_as will
740
+ # <b>re-bind</b> as that user with the password (or other authenticator)
741
+ # that you supply.
742
+ #
743
+ # #bind_as takes the same parameters as #search, <i>with the addition of
744
+ # an authenticator.</i> Currently, this authenticator must be
745
+ # <tt>:password</tt>. Its value may be either a String, or a +proc+ that
746
+ # returns a String. #bind_as returns +false+ on failure. On success, it
747
+ # returns a result set, just as #search does. This result set is an Array
748
+ # of objects of type Net::LDAP::Entry. It contains the directory
749
+ # attributes corresponding to the user. (Just test whether the return
750
+ # value is logically true, if you don't need this additional information.)
751
+ #
752
+ # Here's how you would use #bind_as to authenticate an email address and
753
+ # password:
754
+ #
755
+ # require 'net/ldap'
756
+ #
757
+ # user, psw = "joe_user@yourcompany.com", "joes_psw"
758
+ #
759
+ # ldap = Net::LDAP.new
760
+ # ldap.host = "192.168.0.100"
761
+ # ldap.port = 389
762
+ # ldap.auth "cn=manager, dc=yourcompany, dc=com", "topsecret"
763
+ #
764
+ # result = ldap.bind_as(:base => "dc=yourcompany, dc=com",
765
+ # :filter => "(mail=#{user})",
766
+ # :password => psw)
767
+ # if result
768
+ # puts "Authenticated #{result.first.dn}"
769
+ # else
770
+ # puts "Authentication FAILED."
771
+ # end
772
+ def bind_as(args = {})
773
+ result = false
774
+ open { |me|
775
+ rs = search args
776
+ if rs and rs.first and dn = rs.first.dn
777
+ password = args[:password]
778
+ password = password.call if password.respond_to?(:call)
779
+ result = rs if bind(:method => :simple, :username => dn,
780
+ :password => password)
781
+ end
782
+ }
783
+ result
784
+ end
785
+
786
+ # Adds a new entry to the remote LDAP server.
787
+ # Supported arguments:
788
+ # :dn :: Full DN of the new entry
789
+ # :attributes :: Attributes of the new entry.
790
+ #
791
+ # The attributes argument is supplied as a Hash keyed by Strings or
792
+ # Symbols giving the attribute name, and mapping to Strings or Arrays of
793
+ # Strings giving the actual attribute values. Observe that most LDAP
794
+ # directories enforce schema constraints on the attributes contained in
795
+ # entries. #add will fail with a server-generated error if your attributes
796
+ # violate the server-specific constraints.
797
+ #
798
+ # Here's an example:
799
+ #
800
+ # dn = "cn=George Smith, ou=people, dc=example, dc=com"
801
+ # attr = {
802
+ # :cn => "George Smith",
803
+ # :objectclass => ["top", "inetorgperson"],
804
+ # :sn => "Smith",
805
+ # :mail => "gsmith@example.com"
806
+ # }
807
+ # Net::LDAP.open(:host => host) do |ldap|
808
+ # ldap.add(:dn => dn, :attributes => attr)
809
+ # end
810
+ def add(args)
811
+ if @open_connection
812
+ @result = @open_connection.add(args)
813
+ else
814
+ @result = 0
815
+ begin
816
+ conn = Connection.new(:host => @host, :port => @port,
817
+ :encryption => @encryption)
818
+ if (@result = conn.bind(args[:auth] || @auth)) == 0
819
+ @result = conn.add(args)
820
+ end
821
+ ensure
822
+ conn.close if conn
823
+ end
824
+ end
825
+ @result == 0
826
+ end
827
+
828
+ # Modifies the attribute values of a particular entry on the LDAP
829
+ # directory. Takes a hash with arguments. Supported arguments are:
830
+ # :dn :: (the full DN of the entry whose attributes are to be modified)
831
+ # :operations :: (the modifications to be performed, detailed next)
832
+ #
833
+ # This method returns True or False to indicate whether the operation
834
+ # succeeded or failed, with extended information available by calling
835
+ # #get_operation_result.
836
+ #
837
+ # Also see #add_attribute, #replace_attribute, or #delete_attribute, which
838
+ # provide simpler interfaces to this functionality.
839
+ #
840
+ # The LDAP protocol provides a full and well thought-out set of operations
841
+ # for changing the values of attributes, but they are necessarily somewhat
842
+ # complex and not always intuitive. If these instructions are confusing or
843
+ # incomplete, please send us email or create a bug report on rubyforge.
844
+ #
845
+ # The :operations parameter to #modify takes an array of
846
+ # operation-descriptors. Each individual operation is specified in one
847
+ # element of the array, and most LDAP servers will attempt to perform the
848
+ # operations in order.
849
+ #
850
+ # Each of the operations appearing in the Array must itself be an Array
851
+ # with exactly three elements: an operator:: must be :add, :replace, or
852
+ # :delete an attribute name:: the attribute name (string or symbol) to
853
+ # modify a value:: either a string or an array of strings.
854
+ #
855
+ # The :add operator will, unsurprisingly, add the specified values to the
856
+ # specified attribute. If the attribute does not already exist, :add will
857
+ # create it. Most LDAP servers will generate an error if you try to add a
858
+ # value that already exists.
859
+ #
860
+ # :replace will erase the current value(s) for the specified attribute, if
861
+ # there are any, and replace them with the specified value(s).
862
+ #
863
+ # :delete will remove the specified value(s) from the specified attribute.
864
+ # If you pass nil, an empty string, or an empty array as the value
865
+ # parameter to a :delete operation, the _entire_ _attribute_ will be
866
+ # deleted, along with all of its values.
867
+ #
868
+ # For example:
869
+ #
870
+ # dn = "mail=modifyme@example.com, ou=people, dc=example, dc=com"
871
+ # ops = [
872
+ # [:add, :mail, "aliasaddress@example.com"],
873
+ # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
874
+ # [:delete, :sn, nil]
875
+ # ]
876
+ # ldap.modify :dn => dn, :operations => ops
877
+ #
878
+ # <i>(This example is contrived since you probably wouldn't add a mail
879
+ # value right before replacing the whole attribute, but it shows that
880
+ # order of execution matters. Also, many LDAP servers won't let you delete
881
+ # SN because that would be a schema violation.)</i>
882
+ #
883
+ # It's essential to keep in mind that if you specify more than one
884
+ # operation in a call to #modify, most LDAP servers will attempt to
885
+ # perform all of the operations in the order you gave them. This matters
886
+ # because you may specify operations on the same attribute which must be
887
+ # performed in a certain order.
888
+ #
889
+ # Most LDAP servers will _stop_ processing your modifications if one of
890
+ # them causes an error on the server (such as a schema-constraint
891
+ # violation). If this happens, you will probably get a result code from
892
+ # the server that reflects only the operation that failed, and you may or
893
+ # may not get extended information that will tell you which one failed.
894
+ # #modify has no notion of an atomic transaction. If you specify a chain
895
+ # of modifications in one call to #modify, and one of them fails, the
896
+ # preceding ones will usually not be "rolled back, " resulting in a
897
+ # partial update. This is a limitation of the LDAP protocol, not of
898
+ # Net::LDAP.
899
+ #
900
+ # The lack of transactional atomicity in LDAP means that you're usually
901
+ # better off using the convenience methods #add_attribute,
902
+ # #replace_attribute, and #delete_attribute, which are are wrappers over
903
+ # #modify. However, certain LDAP servers may provide concurrency
904
+ # semantics, in which the several operations contained in a single #modify
905
+ # call are not interleaved with other modification-requests received
906
+ # simultaneously by the server. It bears repeating that this concurrency
907
+ # does _not_ imply transactional atomicity, which LDAP does not provide.
908
+ def modify(args)
909
+ if @open_connection
910
+ @result = @open_connection.modify(args)
911
+ else
912
+ @result = 0
913
+ begin
914
+ conn = Connection.new(:host => @host, :port => @port,
915
+ :encryption => @encryption)
916
+ if (@result = conn.bind(args[:auth] || @auth)) == 0
917
+ @result = conn.modify(args)
918
+ end
919
+ ensure
920
+ conn.close if conn
921
+ end
922
+ end
923
+ @result == 0
924
+ end
925
+
926
+ # Add a value to an attribute. Takes the full DN of the entry to modify,
927
+ # the name (Symbol or String) of the attribute, and the value (String or
928
+ # Array). If the attribute does not exist (and there are no schema
929
+ # violations), #add_attribute will create it with the caller-specified
930
+ # values. If the attribute already exists (and there are no schema
931
+ # violations), the caller-specified values will be _added_ to the values
932
+ # already present.
933
+ #
934
+ # Returns True or False to indicate whether the operation succeeded or
935
+ # failed, with extended information available by calling
936
+ # #get_operation_result. See also #replace_attribute and
937
+ # #delete_attribute.
938
+ #
939
+ # dn = "cn=modifyme, dc=example, dc=com"
940
+ # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
941
+ def add_attribute(dn, attribute, value)
942
+ modify(:dn => dn, :operations => [[:add, attribute, value]])
943
+ end
944
+
945
+ # Replace the value of an attribute. #replace_attribute can be thought of
946
+ # as equivalent to calling #delete_attribute followed by #add_attribute.
947
+ # It takes the full DN of the entry to modify, the name (Symbol or String)
948
+ # of the attribute, and the value (String or Array). If the attribute does
949
+ # not exist, it will be created with the caller-specified value(s). If the
950
+ # attribute does exist, its values will be _discarded_ and replaced with
951
+ # the caller-specified values.
952
+ #
953
+ # Returns True or False to indicate whether the operation succeeded or
954
+ # failed, with extended information available by calling
955
+ # #get_operation_result. See also #add_attribute and #delete_attribute.
956
+ #
957
+ # dn = "cn=modifyme, dc=example, dc=com"
958
+ # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
959
+ def replace_attribute(dn, attribute, value)
960
+ modify(:dn => dn, :operations => [[:replace, attribute, value]])
961
+ end
962
+
963
+ # Delete an attribute and all its values. Takes the full DN of the entry
964
+ # to modify, and the name (Symbol or String) of the attribute to delete.
965
+ #
966
+ # Returns True or False to indicate whether the operation succeeded or
967
+ # failed, with extended information available by calling
968
+ # #get_operation_result. See also #add_attribute and #replace_attribute.
969
+ #
970
+ # dn = "cn=modifyme, dc=example, dc=com"
971
+ # ldap.delete_attribute dn, :mail
972
+ def delete_attribute(dn, attribute)
973
+ modify(:dn => dn, :operations => [[:delete, attribute, nil]])
974
+ end
975
+
976
+ # Rename an entry on the remote DIS by changing the last RDN of its DN.
977
+ #
978
+ # _Documentation_ _stub_
979
+ def rename(args)
980
+ if @open_connection
981
+ @result = @open_connection.rename(args)
982
+ else
983
+ @result = 0
984
+ begin
985
+ conn = Connection.new(:host => @host, :port => @port,
986
+ :encryption => @encryption)
987
+ if (@result = conn.bind(args[:auth] || @auth)) == 0
988
+ @result = conn.rename(args)
989
+ end
990
+ ensure
991
+ conn.close if conn
992
+ end
993
+ end
994
+ @result == 0
995
+ end
996
+ alias_method :modify_rdn, :rename
997
+
998
+ # Delete an entry from the LDAP directory. Takes a hash of arguments. The
999
+ # only supported argument is :dn, which must give the complete DN of the
1000
+ # entry to be deleted.
1001
+ #
1002
+ # Returns True or False to indicate whether the delete succeeded. Extended
1003
+ # status information is available by calling #get_operation_result.
1004
+ #
1005
+ # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1006
+ # ldap.delete :dn => dn
1007
+ def delete(args)
1008
+ if @open_connection
1009
+ @result = @open_connection.delete(args)
1010
+ else
1011
+ @result = 0
1012
+ begin
1013
+ conn = Connection.new(:host => @host, :port => @port,
1014
+ :encryption => @encryption)
1015
+ if (@result = conn.bind(args[:auth] || @auth)) == 0
1016
+ @result = conn.delete(args)
1017
+ end
1018
+ ensure
1019
+ conn.close
1020
+ end
1021
+ end
1022
+ @result == 0
1023
+ end
1024
+
1025
+ # This method is experimental and subject to change. Return the rootDSE
1026
+ # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
1027
+ # the server doesn't return the record.
1028
+ #--
1029
+ # cf. RFC4512 graf 5.1.
1030
+ # Note that the rootDSE record we return on success has an empty DN, which
1031
+ # is correct. On failure, the empty Entry will have a nil DN. There's no
1032
+ # real reason for that, so it can be changed if desired. The funky
1033
+ # number-disagreements in the set of attribute names is correct per the
1034
+ # RFC. We may be called by #search itself, which may need to determine
1035
+ # things like paged search capabilities. So to avoid an infinite regress,
1036
+ # set :ignore_server_caps, which prevents us getting called recursively.
1037
+ #++
1038
+ def search_root_dse
1039
+ rs = search(:ignore_server_caps => true, :base => "",
1040
+ :scope => SearchScope_BaseObject,
1041
+ :attributes => [ :namingContexts, :supportedLdapVersion,
1042
+ :altServer, :supportedControl, :supportedExtension,
1043
+ :supportedFeatures, :supportedSASLMechanisms])
1044
+ (rs and rs.first) or Net::LDAP::Entry.new
1045
+ end
1046
+
1047
+ # Return the root Subschema record from the LDAP server as a
1048
+ # Net::LDAP::Entry, or an empty Entry if the server doesn't return the
1049
+ # record. On success, the Net::LDAP::Entry returned from this call will
1050
+ # have the attributes :dn, :objectclasses, and :attributetypes. If there
1051
+ # is an error, call #get_operation_result for more information.
1052
+ #
1053
+ # ldap = Net::LDAP.new
1054
+ # ldap.host = "your.ldap.host"
1055
+ # ldap.auth "your-user-dn", "your-psw"
1056
+ # subschema_entry = ldap.search_subschema_entry
1057
+ #
1058
+ # subschema_entry.attributetypes.each do |attrtype|
1059
+ # # your code
1060
+ # end
1061
+ #
1062
+ # subschema_entry.objectclasses.each do |attrtype|
1063
+ # # your code
1064
+ # end
1065
+ #--
1066
+ # cf. RFC4512 section 4, particulary graff 4.4.
1067
+ # The :dn attribute in the returned Entry is the subschema name as
1068
+ # returned from the server. Set :ignore_server_caps, see the notes in
1069
+ # search_root_dse.
1070
+ #++
1071
+ def search_subschema_entry
1072
+ rs = search(:ignore_server_caps => true, :base => "",
1073
+ :scope => SearchScope_BaseObject,
1074
+ :attributes => [:subschemaSubentry])
1075
+ return Net::LDAP::Entry.new unless (rs and rs.first)
1076
+
1077
+ subschema_name = rs.first.subschemasubentry
1078
+ return Net::LDAP::Entry.new unless (subschema_name and subschema_name.first)
1079
+
1080
+ rs = search(:ignore_server_caps => true, :base => subschema_name.first,
1081
+ :scope => SearchScope_BaseObject,
1082
+ :filter => "objectclass=subschema",
1083
+ :attributes => [:objectclasses, :attributetypes])
1084
+ (rs and rs.first) or Net::LDAP::Entry.new
1085
+ end
1086
+
1087
+ #--
1088
+ # Convenience method to query server capabilities.
1089
+ # Only do this once per Net::LDAP object.
1090
+ # Note, we call a search, and we might be called from inside a search!
1091
+ # MUST refactor the root_dse call out.
1092
+ #++
1093
+ def paged_searches_supported?
1094
+ @server_caps ||= search_root_dse
1095
+ @server_caps[:supportedcontrol].include?(Net::LDAP::LdapControls::PagedResults)
1096
+ end
1097
+ end # class LDAP
1098
+
1099
+ # This is a private class used internally by the library. It should not
1100
+ # be called by user code.
1101
+ class Net::LDAP::Connection #:nodoc:
1102
+ LdapVersion = 3
1103
+ MaxSaslChallenges = 10
1104
+
1105
+ def initialize(server)
1106
+ begin
1107
+ @conn = TCPSocket.new(server[:host], server[:port])
1108
+ rescue SocketError
1109
+ raise Net::LDAP::LdapError, "No such address or other socket error."
1110
+ rescue Errno::ECONNREFUSED
1111
+ raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."
1112
+ end
1113
+
1114
+ if server[:encryption]
1115
+ setup_encryption server[:encryption]
1116
+ end
1117
+
1118
+ yield self if block_given?
1119
+ end
1120
+
1121
+ module GetbyteForSSLSocket
1122
+ def getbyte
1123
+ getc.ord
1124
+ end
1125
+ end
1126
+
1127
+ def self.wrap_with_ssl(io)
1128
+ raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
1129
+ ctx = OpenSSL::SSL::SSLContext.new
1130
+ conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
1131
+ conn.connect
1132
+ conn.sync_close = true
1133
+
1134
+ conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
1135
+
1136
+ conn
1137
+ end
1138
+
1139
+ #--
1140
+ # Helper method called only from new, and only after we have a
1141
+ # successfully-opened @conn instance variable, which is a TCP connection.
1142
+ # Depending on the received arguments, we establish SSL, potentially
1143
+ # replacing the value of @conn accordingly. Don't generate any errors here
1144
+ # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption
1145
+ # is requested and we have trouble setting it up. That includes if OpenSSL
1146
+ # is not set up on the machine. (Question: how does the Ruby OpenSSL
1147
+ # wrapper react in that case?) DO NOT filter exceptions raised by the
1148
+ # OpenSSL library. Let them pass back to the user. That should make it
1149
+ # easier for us to debug the problem reports. Presumably (hopefully?) that
1150
+ # will also produce recognizable errors if someone tries to use this on a
1151
+ # machine without OpenSSL.
1152
+ #
1153
+ # The simple_tls method is intended as the simplest, stupidest, easiest
1154
+ # solution for people who want nothing more than encrypted comms with the
1155
+ # LDAP server. It doesn't do any server-cert validation and requires
1156
+ # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
1157
+ # WE REPLACE the value of @conn, which is presumed to be a connected
1158
+ # TCPSocket object.
1159
+ #
1160
+ # The start_tls method is supported by many servers over the standard LDAP
1161
+ # port. It does not require an alternative port for encrypted
1162
+ # communications, as with simple_tls. Thanks for Kouhei Sutou for
1163
+ # generously contributing the :start_tls path.
1164
+ #++
1165
+ def setup_encryption(args)
1166
+ case args[:method]
1167
+ when :simple_tls
1168
+ @conn = self.class.wrap_with_ssl(@conn)
1169
+ # additional branches requiring server validation and peer certs, etc.
1170
+ # go here.
1171
+ when :start_tls
1172
+ msgid = next_msgid.to_ber
1173
+ request = [StartTlsOid.to_ber].to_ber_appsequence(Net::LdapPdu::ExtendedRequest)
1174
+ request_pkt = [msgid, request].to_ber_sequence
1175
+ @conn.write request_pkt
1176
+ be = @conn.read_ber(Net::LDAP::AsnSyntax)
1177
+ raise Net::LDAP::LdapError, "no start_tls result" if be.nil?
1178
+ pdu = Net::LdapPdu.new(be)
1179
+ raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil?
1180
+ if pdu.result_code.zero?
1181
+ @conn = self.class.wrap_with_ssl(@conn)
1182
+ else
1183
+ raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}"
1184
+ end
1185
+ else
1186
+ raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}"
1187
+ end
1188
+ end
1189
+
1190
+ #--
1191
+ # This is provided as a convenience method to make sure a connection
1192
+ # object gets closed without waiting for a GC to happen. Clients shouldn't
1193
+ # have to call it, but perhaps it will come in handy someday.
1194
+ #++
1195
+ def close
1196
+ @conn.close
1197
+ @conn = nil
1198
+ end
1199
+
1200
+ def next_msgid
1201
+ @msgid ||= 0
1202
+ @msgid += 1
1203
+ end
1204
+
1205
+ def bind(auth)
1206
+ meth = auth[:method]
1207
+ if [:simple, :anonymous, :anon].include?(meth)
1208
+ bind_simple auth
1209
+ elsif meth == :sasl
1210
+ bind_sasl(auth)
1211
+ elsif meth == :gss_spnego
1212
+ bind_gss_spnego(auth)
1213
+ else
1214
+ raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})"
1215
+ end
1216
+ end
1217
+
1218
+ #--
1219
+ # Implements a simple user/psw authentication. Accessed by calling #bind
1220
+ # with a method of :simple or :anonymous.
1221
+ #++
1222
+ def bind_simple(auth)
1223
+ user, psw = if auth[:method] == :simple
1224
+ [auth[:username] || auth[:dn], auth[:password]]
1225
+ else
1226
+ ["", ""]
1227
+ end
1228
+
1229
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1230
+
1231
+ msgid = next_msgid.to_ber
1232
+ request = [LdapVersion.to_ber, user.to_ber,
1233
+ psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
1234
+ request_pkt = [msgid, request].to_ber_sequence
1235
+ @conn.write request_pkt
1236
+
1237
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LdapPdu.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1238
+
1239
+ pdu.result_code
1240
+ end
1241
+
1242
+ #--
1243
+ # Required parameters: :mechanism, :initial_credential and
1244
+ # :challenge_response
1245
+ #
1246
+ # Mechanism is a string value that will be passed in the SASL-packet's
1247
+ # "mechanism" field.
1248
+ #
1249
+ # Initial credential is most likely a string. It's passed in the initial
1250
+ # BindRequest that goes to the server. In some protocols, it may be empty.
1251
+ #
1252
+ # Challenge-response is a Ruby proc that takes a single parameter and
1253
+ # returns an object that will typically be a string. The
1254
+ # challenge-response block is called when the server returns a
1255
+ # BindResponse with a result code of 14 (saslBindInProgress). The
1256
+ # challenge-response block receives a parameter containing the data
1257
+ # returned by the server in the saslServerCreds field of the LDAP
1258
+ # BindResponse packet. The challenge-response block may be called multiple
1259
+ # times during the course of a SASL authentication, and each time it must
1260
+ # return a value that will be passed back to the server as the credential
1261
+ # data in the next BindRequest packet.
1262
+ #++
1263
+ def bind_sasl(auth)
1264
+ mech, cred, chall = auth[:mechanism], auth[:initial_credential],
1265
+ auth[:challenge_response]
1266
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall)
1267
+
1268
+ n = 0
1269
+ loop {
1270
+ msgid = next_msgid.to_ber
1271
+ sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
1272
+ request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
1273
+ request_pkt = [msgid, request].to_ber_sequence
1274
+ @conn.write request_pkt
1275
+
1276
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LdapPdu.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1277
+ return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
1278
+ raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
1279
+
1280
+ cred = chall.call(pdu.result_server_sasl_creds)
1281
+ }
1282
+
1283
+ raise Net::LDAP::LdapError, "why are we here?"
1284
+ end
1285
+ private :bind_sasl
1286
+
1287
+ #--
1288
+ # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
1289
+ # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
1290
+ # integrate it without introducing an external dependency.
1291
+ #
1292
+ # This authentication method is accessed by calling #bind with a :method
1293
+ # parameter of :gss_spnego. It requires :username and :password
1294
+ # attributes, just like the :simple authentication method. It performs a
1295
+ # GSS-SPNEGO authentication with the server, which is presumed to be a
1296
+ # Microsoft Active Directory.
1297
+ #++
1298
+ def bind_gss_spnego(auth)
1299
+ require 'ntlm'
1300
+
1301
+ user, psw = [auth[:username] || auth[:dn], auth[:password]]
1302
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1303
+
1304
+ nego = proc { |challenge|
1305
+ t2_msg = NTLM::Message.parse(challenge)
1306
+ t3_msg = t2_msg.response({ :user => user, :password => psw },
1307
+ { :ntlmv2 => true })
1308
+ t3_msg.serialize
1309
+ }
1310
+
1311
+ bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
1312
+ :initial_credential => NTLM::Message::Type1.new.serialize,
1313
+ :challenge_response => nego)
1314
+ end
1315
+ private :bind_gss_spnego
1316
+
1317
+ #--
1318
+ # Alternate implementation, this yields each search entry to the caller as
1319
+ # it are received.
1320
+ #
1321
+ # TODO: certain search parameters are hardcoded.
1322
+ # TODO: if we mis-parse the server results or the results are wrong, we
1323
+ # can block forever. That's because we keep reading results until we get a
1324
+ # type-5 packet, which might never come. We need to support the time-limit
1325
+ # in the protocol.
1326
+ #++
1327
+ def search(args = {})
1328
+ search_filter = (args && args[:filter]) ||
1329
+ Net::LDAP::Filter.eq("objectclass", "*")
1330
+ search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String)
1331
+ search_base = (args && args[:base]) || "dc=example, dc=com"
1332
+ search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber}
1333
+ return_referrals = args && args[:return_referrals] == true
1334
+ sizelimit = (args && args[:size].to_i) || 0
1335
+ raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0
1336
+ paged_searches_supported = (args && args[:paged_searches_supported])
1337
+
1338
+ attributes_only = (args and args[:attributes_only] == true)
1339
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
1340
+ raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
1341
+
1342
+ # An interesting value for the size limit would be close to A/D's
1343
+ # built-in page limit of 1000 records, but openLDAP newer than version
1344
+ # 2.2.0 chokes on anything bigger than 126. You get a silent error that
1345
+ # is easily visible by running slapd in debug mode. Go figure.
1346
+ #
1347
+ # Changed this around 06Sep06 to support a caller-specified search-size
1348
+ # limit. Because we ALWAYS do paged searches, we have to work around the
1349
+ # problem that it's not legal to specify a "normal" sizelimit (in the
1350
+ # body of the search request) that is larger than the page size we're
1351
+ # requesting. Unfortunately, I have the feeling that this will break
1352
+ # with LDAP servers that don't support paged searches!!!
1353
+ #
1354
+ # (Because we pass zero as the sizelimit on search rounds when the
1355
+ # remaining limit is larger than our max page size of 126. In these
1356
+ # cases, I think the caller's search limit will be ignored!)
1357
+ #
1358
+ # CONFIRMED: This code doesn't work on LDAPs that don't support paged
1359
+ # searches when the size limit is larger than 126. We're going to have
1360
+ # to do a root-DSE record search and not do a paged search if the LDAP
1361
+ # doesn't support it. Yuck.
1362
+ rfc2696_cookie = [126, ""]
1363
+ result_code = 0
1364
+ n_results = 0
1365
+
1366
+ loop {
1367
+ # should collect this into a private helper to clarify the structure
1368
+ query_limit = 0
1369
+ if sizelimit > 0
1370
+ if paged_searches_supported
1371
+ query_limit = (((sizelimit - n_results) < 126) ? (sizelimit -
1372
+ n_results) : 0)
1373
+ else
1374
+ query_limit = sizelimit
1375
+ end
1376
+ end
1377
+
1378
+ request = [
1379
+ search_base.to_ber,
1380
+ scope.to_ber_enumerated,
1381
+ 0.to_ber_enumerated,
1382
+ query_limit.to_ber, # size limit
1383
+ 0.to_ber,
1384
+ attributes_only.to_ber,
1385
+ search_filter.to_ber,
1386
+ search_attributes.to_ber_sequence
1387
+ ].to_ber_appsequence(3)
1388
+
1389
+ controls = [
1390
+ [
1391
+ Net::LDAP::LdapControls::PagedResults.to_ber,
1392
+ # Criticality MUST be false to interoperate with normal LDAPs.
1393
+ false.to_ber,
1394
+ rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
1395
+ ].to_ber_sequence
1396
+ ].to_ber_contextspecific(0)
1397
+
1398
+ pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
1399
+ @conn.write pkt
1400
+
1401
+ result_code = 0
1402
+ controls = []
1403
+
1404
+ while (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LdapPdu.new(be))
1405
+ case pdu.app_tag
1406
+ when 4 # search-data
1407
+ n_results += 1
1408
+ yield pdu.search_entry if block_given?
1409
+ when 19 # search-referral
1410
+ if return_referrals
1411
+ if block_given?
1412
+ se = Net::LDAP::Entry.new
1413
+ se[:search_referrals] = (pdu.search_referrals || [])
1414
+ yield se
1415
+ end
1416
+ end
1417
+ when 5 # search-result
1418
+ result_code = pdu.result_code
1419
+ controls = pdu.result_controls
1420
+ break
1421
+ else
1422
+ raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}"
1423
+ end
1424
+ end
1425
+
1426
+ # When we get here, we have seen a type-5 response. If there is no
1427
+ # error AND there is an RFC-2696 cookie, then query again for the next
1428
+ # page of results. If not, we're done. Don't screw this up or we'll
1429
+ # break every search we do.
1430
+ #
1431
+ # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
1432
+ # that have a parameter of AsnSyntax? Does this just accidentally
1433
+ # work? According to RFC-2696, the value expected in this position is
1434
+ # of type OCTET STRING, covered in the default syntax supported by
1435
+ # read_ber, so I guess we're ok.
1436
+ more_pages = false
1437
+ if result_code == 0 and controls
1438
+ controls.each do |c|
1439
+ if c.oid == Net::LDAP::LdapControls::PagedResults
1440
+ # just in case some bogus server sends us more than 1 of these.
1441
+ more_pages = false
1442
+ if c.value and c.value.length > 0
1443
+ cookie = c.value.read_ber[1]
1444
+ if cookie and cookie.length > 0
1445
+ rfc2696_cookie[1] = cookie
1446
+ more_pages = true
1447
+ end
1448
+ end
1449
+ end
1450
+ end
1451
+ end
1452
+
1453
+ break unless more_pages
1454
+ } # loop
1455
+
1456
+ result_code
1457
+ end
1458
+
1459
+ #--
1460
+ # TODO: need to support a time limit, in case the server fails to respond.
1461
+ # TODO: We're throwing an exception here on empty DN. Should return a
1462
+ # proper error instead, probaby from farther up the chain.
1463
+ # TODO: If the user specifies a bogus opcode, we'll throw a confusing
1464
+ # error here ("to_ber_enumerated is not defined on nil").
1465
+ #++
1466
+ def modify(args)
1467
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
1468
+ modify_ops = []
1469
+ a = args[:operations] and a.each { |op, attr, values|
1470
+ # TODO, fix the following line, which gives a bogus error if the
1471
+ # opcode is invalid.
1472
+ op_1 = { :add => 0, :delete => 1, :replace => 2 }[op.to_sym].to_ber_enumerated
1473
+ modify_ops << [op_1, [attr.to_s.to_ber, Array(values).map { |v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
1474
+ }
1475
+
1476
+ request = [modify_dn.to_ber,
1477
+ modify_ops.to_ber_sequence].to_ber_appsequence(6)
1478
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1479
+ @conn.write pkt
1480
+
1481
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LdapPdu.new(be)) && (pdu.app_tag == 7) or raise Net::LDAP::LdapError, "response missing or invalid"
1482
+ pdu.result_code
1483
+ end
1484
+
1485
+ #--
1486
+ # TODO: need to support a time limit, in case the server fails to respond.
1487
+ # Unlike other operation-methods in this class, we return a result hash
1488
+ # rather than a simple result number. This is experimental, and eventually
1489
+ # we'll want to do this with all the others. The point is to have access
1490
+ # to the error message and the matched-DN returned by the server.
1491
+ #++
1492
+ def add(args)
1493
+ add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN"
1494
+ add_attrs = []
1495
+ a = args[:attributes] and a.each { |k, v|
1496
+ add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
1497
+ }
1498
+
1499
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
1500
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1501
+ @conn.write pkt
1502
+
1503
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LdapPdu.new(be)) && (pdu.app_tag == 9) or raise Net::LDAP::LdapError, "response missing or invalid"
1504
+ pdu.result_code
1505
+ end
1506
+
1507
+ #--
1508
+ # TODO: need to support a time limit, in case the server fails to respond.
1509
+ #++
1510
+ def rename(args)
1511
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
1512
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1513
+ delete_attrs = args[:delete_attributes] ? true : false
1514
+
1515
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
1516
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1517
+ @conn.write pkt
1518
+
1519
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LdapPdu.new(be)) && (pdu.app_tag == 13) or raise Net::LDAP::LdapError, "response missing or invalid"
1520
+ pdu.result_code
1521
+ end
1522
+
1523
+ #--
1524
+ # TODO, need to support a time limit, in case the server fails to respond.
1525
+ #++
1526
+ def delete(args)
1527
+ dn = args[:dn] or raise "Unable to delete empty DN"
1528
+
1529
+ request = dn.to_s.to_ber_application_string(10)
1530
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1531
+ @conn.write pkt
1532
+
1533
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LdapPdu.new(be)) && (pdu.app_tag == 11) or raise Net::LDAP::LdapError, "response missing or invalid"
1534
+ pdu.result_code
1535
+ end
1536
+ end # class Connection