net-ldap 0.0.5

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.

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