ruby-net-ldap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1041 @@
1
+ # $Id: ldap.rb 94 2006-05-01 07:19:12Z blackhedd $
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
+ require 'net/ber'
22
+ require 'net/ldap/pdu'
23
+ require 'net/ldap/filter'
24
+ require 'net/ldap/dataset'
25
+ require 'net/ldap/psw'
26
+ require 'net/ldap/entry'
27
+
28
+
29
+ module Net
30
+
31
+
32
+ # == Net::LDAP
33
+ #
34
+ # This library provides a pure-Ruby implementation of the
35
+ # LDAP client protocol, per RFC-1777.
36
+ # It can be used to access any server which implements the
37
+ # LDAP protocol.
38
+ #
39
+ # Net::LDAP is intended to provide full LDAP functionality
40
+ # while hiding the more arcane aspects
41
+ # the LDAP protocol itself, and thus presenting as Ruby-like
42
+ # a programming interface as possible.
43
+ #
44
+ # === Quick-start for the Impatient
45
+ # require 'rubygems'
46
+ # require 'net/ldap'
47
+ #
48
+ # ldap = Net::LDAP.new :host => server_ip_address,
49
+ # :port => 389,
50
+ # :auth => {
51
+ # :method => :simple,
52
+ # :username => "cn=manager,dc=example,dc=com",
53
+ # :password => "opensesame"
54
+ # }
55
+ #
56
+ # filter = Net::LDAP::Filter.eq( "cn", "George*" )
57
+ # treebase = "dc=example,dc=com"
58
+ #
59
+ # ldap.search( :base => treebase, :filter => filter ) do |entry|
60
+ # puts "DN: #{entry.dn}"
61
+ # entry.each do |attribute, values|
62
+ # puts " #{attribute}:"
63
+ # values.each do |value|
64
+ # puts " --->#{value}"
65
+ # end
66
+ # end
67
+ # end
68
+ #
69
+ # p ldap.get_operation_result
70
+ #
71
+ #
72
+ # == Quick introduction to LDAP
73
+ #
74
+ # We're going to provide a quick and highly informal introduction to LDAP
75
+ # terminology and
76
+ # typical operations. If you're comfortable with this material, skip
77
+ # ahead to "How to use Net::LDAP." If you want a more rigorous treatment
78
+ # of this material, we recommend you start with the various IETF and ITU
79
+ # standards that control LDAP.
80
+ #
81
+ # === Entities
82
+ # LDAP is an Internet-standard protocol used to access directory servers.
83
+ # The basic search unit is the <i>entity,</i> which corresponds to
84
+ # a person or other domain-specific object.
85
+ # A directory service which supports the LDAP protocol typically
86
+ # stores information about a number of entities.
87
+ #
88
+ # === Principals
89
+ # LDAP servers are typically used to access information about people,
90
+ # but also very often about such items as printers, computers, and other
91
+ # resources. To reflect this, LDAP uses the term <i>entity,</i> or less
92
+ # commonly, <i>principal,</i> to denote its basic data-storage unit.
93
+ #
94
+ #
95
+ # === Distinguished Names
96
+ # In LDAP's view of the world,
97
+ # an entity is uniquely identified by a globally-unique text string
98
+ # called a <i>Distinguished Name,</i> originally defined in the X.400
99
+ # standards from which LDAP is ultimately derived.
100
+ # Much like a DNS hostname, a DN is a "flattened" text representation
101
+ # of a string of tree nodes. Also like DNS (and unlike Java package
102
+ # names), a DN expresses a chain of tree-nodes written from left to right
103
+ # in order from the most-resolved node to the most-general one.
104
+ #
105
+ # If you know the DN of a person or other entity, then you can query
106
+ # an LDAP-enabled directory for information (attributes) about the entity.
107
+ # Alternatively, you can query the directory for a list of DNs matching
108
+ # a set of criteria that you supply.
109
+ #
110
+ # === Attributes
111
+ #
112
+ # In the LDAP view of the world, a DN uniquely identifies an entity.
113
+ # Information about the entity is stored as a set of <i>Attributes.</i>
114
+ # An attribute is a text string which is associated with zero or more
115
+ # values. Most LDAP-enabled directories store a well-standardized
116
+ # range of attributes, and constrain their values according to standard
117
+ # rules.
118
+ #
119
+ # A good example of an attribute is <tt>cn,</tt> which stands for "Common Name."
120
+ # In many directories, this attribute is used to store a string consisting of
121
+ # a person's first and last names. Most directories enforce the convention that
122
+ # an entity's <tt>cn</tt> attribute have <i>exactly one</i> value. In LDAP
123
+ # jargon, that means that <tt>cn</tt> must be <i>present</i> and
124
+ # <i>single-valued.</i>
125
+ #
126
+ # Another attribute is <tt>mail,</tt> which is used to store email addresses.
127
+ # (No, there is no attribute called "email," perhaps because X.400 terminology
128
+ # predates the invention of the term <i>email.</i>) <tt>mail</tt> differs
129
+ # from <tt>cn</tt> in that most directories permit any number of values for the
130
+ # <tt>mail</tt> attribute, including zero.
131
+ #
132
+ #
133
+ # === Tree-Base
134
+ # We said above that X.400 Distinguished Names are <i>globally unique.</i>
135
+ # In a manner reminiscent of DNS, LDAP supposes that each directory server
136
+ # contains authoritative attribute data for a set of DNs corresponding
137
+ # to a specific sub-tree of the (notional) global directory tree.
138
+ # This subtree is generally configured into a directory server when it is
139
+ # created. It matters for this discussion because most servers will not
140
+ # allow you to query them unless you specify a correct tree-base.
141
+ #
142
+ # Let's say you work for the engineering department of Big Company, Inc.,
143
+ # whose internet domain is bigcompany.com. You may find that your departmental
144
+ # directory is stored in a server with a defined tree-base of
145
+ # ou=engineering,dc=bigcompany,dc=com
146
+ # You will need to supply this string as the <i>tree-base</i> when querying this
147
+ # directory. (Ou is a very old X.400 term meaning "organizational unit."
148
+ # Dc is a more recent term meaning "domain component.")
149
+ #
150
+ # === LDAP Versions
151
+ # (stub, discuss v2 and v3)
152
+ #
153
+ # === LDAP Operations
154
+ # The essential operations are: #bind, #search, #add, #modify, #delete, and #rename.
155
+ # ==== Bind
156
+ # #bind supplies a user's authentication credentials to a server, which in turn verifies
157
+ # or rejects them. There is a range of possibilities for credentials, but most directories
158
+ # support a simple username and password authentication.
159
+ #
160
+ # Taken by itself, #bind can be used to authenticate a user against information
161
+ # stored in a directory, for example to permit or deny access to some other resource.
162
+ # In terms of the other LDAP operations, most directories require a successful #bind to
163
+ # be performed before the other operations will be permitted. Some servers permit certain
164
+ # operations to be performed with an "anonymous" binding, meaning that no credentials are
165
+ # presented by the user. (We're glossing over a lot of platform-specific detail here.)
166
+ #
167
+ # ==== Search
168
+ # Calling #search against the directory involves specifying a treebase, a set of <i>search filters,</i>
169
+ # and a list of attribute values.
170
+ # The filters specify ranges of possible values for particular attributes. Multiple
171
+ # filters can be joined together with AND, OR, and NOT operators.
172
+ # A server will respond to a #search by returning a list of matching DNs together with a
173
+ # set of attribute values for each entity, depending on what attributes the search requested.
174
+ #
175
+ # ==== Add
176
+ # #add operation specifies a new DN and an initial set of attribute values. If the operation
177
+ # succeeds, a new entity with the corresponding DN and attributes is added to the directory.
178
+ #
179
+ # ==== Modify
180
+ # #modify specifies an entity DN, and a list of attribute operations. #modify is used to change
181
+ # the attribute values stored in the directory for a particular entity.
182
+ # #modify may add or delete attributes (which are lists of values) or it change attributes by
183
+ # adding to or deleting from their values.
184
+ # There are three easier methods to modify an entry's attribute values:
185
+ # #add_attribute, #replace_attribute, and #delete_attribute.
186
+ #
187
+ # ==== Delete
188
+ # #delete operation specifies an entity DN. If it succeeds, the entity and all its attributes
189
+ # is removed from the directory.
190
+ #
191
+ # ==== Rename (or Modify RDN)
192
+ # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP protocol. It responds to
193
+ # the often-arising need to change the DN of an entity without discarding its attribute values.
194
+ # In earlier LDAP versions, the only way to do this was to delete the whole entity and add it
195
+ # again with a different DN.
196
+ #
197
+ # #rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most
198
+ # part of the DN string. If successful, #rename changes the entity DN so that its left-most
199
+ # node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name,"
200
+ # denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.)
201
+ #
202
+ # == How to use Net::LDAP
203
+ #
204
+ # To access Net::LDAP functionality in your Ruby programs, start by requiring
205
+ # the library:
206
+ #
207
+ # require 'net/ldap'
208
+ #
209
+ # If you installed the Gem version of Net::LDAP, and depending on your version of
210
+ # Ruby and rubygems, you _may_ also need to require rubygems explicitly:
211
+ #
212
+ # require 'rubygems'
213
+ # require 'net/ldap'
214
+ #
215
+ # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
216
+ # The constructor for this object takes arguments specifying the network location
217
+ # (address and port) of the LDAP server, and also the binding (authentication)
218
+ # credentials, typically a username and password.
219
+ # Given an object of class Net:LDAP, you can then perform LDAP operations by calling
220
+ # instance methods on the object. These are documented with usage examples below.
221
+ #
222
+ # The Net::LDAP library is designed to be very disciplined about how it makes network
223
+ # connections to servers. This is different from many of the standard native-code
224
+ # libraries that are provided on most platforms, which share bloodlines with the
225
+ # original Netscape/Michigan LDAP client implementations. These libraries sought to
226
+ # insulate user code from the workings of the network. This is a good idea of course,
227
+ # but the practical effect has been confusing and many difficult bugs have been caused
228
+ # by the opacity of the native libraries, and their variable behavior across platforms.
229
+ #
230
+ # In general, Net::LDAP instance methods which invoke server operations make a connection
231
+ # to the server when the method is called. They execute the operation (typically binding first)
232
+ # and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection
233
+ # to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open
234
+ # closes the connection on completion of the block.
235
+ #
236
+
237
+ class LDAP
238
+
239
+ class LdapError < Exception; end
240
+
241
+ VERSION = "0.0.1"
242
+
243
+
244
+ SearchScope_BaseObject = 0
245
+ SearchScope_SingleLevel = 1
246
+ SearchScope_WholeSubtree = 2
247
+ SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree]
248
+
249
+ AsnSyntax = {
250
+ :application => {
251
+ :constructed => {
252
+ 0 => :array, # BindRequest
253
+ 1 => :array, # BindResponse
254
+ 2 => :array, # UnbindRequest
255
+ 3 => :array, # SearchRequest
256
+ 4 => :array, # SearchData
257
+ 5 => :array, # SearchResult
258
+ 6 => :array, # ModifyRequest
259
+ 7 => :array, # ModifyResponse
260
+ 8 => :array, # AddRequest
261
+ 9 => :array, # AddResponse
262
+ 10 => :array, # DelRequest
263
+ 11 => :array, # DelResponse
264
+ 12 => :array, # ModifyRdnRequest
265
+ 13 => :array, # ModifyRdnResponse
266
+ 14 => :array, # CompareRequest
267
+ 15 => :array, # CompareResponse
268
+ 16 => :array, # AbandonRequest
269
+ 24 => :array, # Unsolicited Notification
270
+ }
271
+ },
272
+ :context_specific => {
273
+ :primitive => {
274
+ 0 => :string, # password
275
+ 1 => :string, # Kerberos v4
276
+ 2 => :string, # Kerberos v5
277
+ }
278
+ }
279
+ }
280
+
281
+ DefaultHost = "127.0.0.1"
282
+ DefaultPort = 389
283
+ DefaultAuth = {:method => :anonymous}
284
+
285
+
286
+ ResultStrings = {
287
+ 0 => "Success",
288
+ 1 => "Operations Error",
289
+ 2 => "Protocol Error",
290
+ 16 => "No Such Attribute",
291
+ 17 => "Undefined Attribute Type",
292
+ 20 => "Attribute or Value Exists",
293
+ 32 => "No Such Object",
294
+ 34 => "Invalid DN Syntax",
295
+ 48 => "Invalid DN Syntax",
296
+ 48 => "Inappropriate Authentication",
297
+ 49 => "Invalid Credentials",
298
+ 50 => "Insufficient Access Rights",
299
+ 51 => "Busy",
300
+ 52 => "Unavailable",
301
+ 53 => "Unwilling to perform",
302
+ 65 => "Object Class Violation",
303
+ 68 => "Entry Already Exists"
304
+ }
305
+
306
+ #
307
+ # LDAP::result2string
308
+ #
309
+ def LDAP::result2string code
310
+ ResultStrings[code] || "unknown result (#{code})"
311
+ end
312
+
313
+ # Instantiate an object of type Net::LDAP to perform directory operations.
314
+ # This constructor takes a Hash containing arguments. The following arguments
315
+ # are supported:
316
+ # * :host => the LDAP server's IP-address (default 127.0.0.1)
317
+ # * :port => the LDAP server's TCP port (default 389)
318
+ # * :auth => a Hash containing authorization parameters. Currently supported values include:
319
+ # {:method => :anonymous} and
320
+ # {:method => :simple, :username => your_user_name, :password => your_password }
321
+ #
322
+ # Instantiating a Net::LDAP object does <i>not</i> result in network traffic to
323
+ # the LDAP server. It simply stores the connection and binding parameters in the
324
+ # object.
325
+ #
326
+ def initialize args = {}
327
+ @host = args[:host] || DefaultHost
328
+ @port = args[:port] || DefaultPort
329
+ @verbose = false # Make this configurable with a switch on the class.
330
+ @auth = args[:auth] || DefaultAuth
331
+
332
+ # This variable is only set when we are created with LDAP::open.
333
+ # All of our internal methods will connect using it, or else
334
+ # they will create their own.
335
+ @open_connection = nil
336
+ end
337
+
338
+ # #open takes the same parameters as #new. #open makes a network connection to the
339
+ # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
340
+ # Within the block, you can call any of the instance methods of Net::LDAP to
341
+ # perform operations against the LDAP directory. #open will perform all the
342
+ # operations in the user-supplied block on the same network connection, which
343
+ # will be closed automatically when the block finishes.
344
+ #
345
+ # # (PSEUDOCODE)
346
+ # auth = {:method => :simple, :username => username, :password => password}
347
+ # Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap|
348
+ # ldap.search( ... )
349
+ # ldap.add( ... )
350
+ # ldap.modify( ... )
351
+ # end
352
+ #
353
+ def LDAP::open args
354
+ ldap1 = LDAP.new args
355
+ ldap1.open {|ldap| yield ldap }
356
+ end
357
+
358
+ # Returns a meaningful result any time after
359
+ # a protocol operation (#bind, #search, #add, #modify, #rename, #delete)
360
+ # has completed.
361
+ # It returns an #OpenStruct containing an LDAP result code (0 means success),
362
+ # and a human-readable string.
363
+ # unless ldap.bind
364
+ # puts "Result: #{ldap.get_operation_result.code}"
365
+ # puts "Message: #{ldap.get_operation_result.message}"
366
+ # end
367
+ #
368
+ def get_operation_result
369
+ os = OpenStruct.new
370
+ if @result
371
+ os.code = @result
372
+ else
373
+ os.code = 0
374
+ end
375
+ os.message = LDAP.result2string( os.code )
376
+ os
377
+ end
378
+
379
+
380
+ # Opens a network connection to the server and then
381
+ # passes <tt>self</tt> to the caller-supplied block. The connection is
382
+ # closed when the block completes. Used for executing multiple
383
+ # LDAP operations without requiring a separate network connection
384
+ # (and authentication) for each one.
385
+ # <i>Note:</i> You do not need to log-in or "bind" to the server. This will
386
+ # be done for you automatically.
387
+ # For an even simpler approach, see the class method Net::LDAP#open.
388
+ #
389
+ # # (PSEUDOCODE)
390
+ # auth = {:method => :simple, :username => username, :password => password}
391
+ # ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth )
392
+ # ldap.open do |ldap|
393
+ # ldap.search( ... )
394
+ # ldap.add( ... )
395
+ # ldap.modify( ... )
396
+ # end
397
+ #--
398
+ # First we make a connection and then a binding, but we don't
399
+ # do anything with the bind results.
400
+ # We then pass self to the caller's block, where he will execute
401
+ # his LDAP operations. Of course they will all generate auth failures
402
+ # if the bind was unsuccessful.
403
+ def open
404
+ raise LdapError.new( "open already in progress" ) if @open_connection
405
+ @open_connection = Connection.new( :host => @host, :port => @port )
406
+ @open_connection.bind @auth
407
+ yield self
408
+ @open_connection.close
409
+ end
410
+
411
+
412
+ # <i>DEPRECATED.</i> Performs an LDAP search, waits for the operation to complete, and
413
+ # passes a result set to the caller-supplied block.
414
+ #--
415
+ # If an open call is in progress (@open_connection will be non-nil),
416
+ # then ASSUME a bind has been performed and accepted, and just
417
+ # execute the search.
418
+ # If @open_connection is nil, then we have to connect, bind,
419
+ # search, and then disconnect. (The disconnect is not strictly
420
+ # necessary but it's friendlier to the network to do it here
421
+ # rather than waiting for Ruby's GC.)
422
+ # Note that in the standalone case, we're permitting the caller
423
+ # to modify the auth parms.
424
+ #
425
+ def searchx args
426
+ if @open_connection
427
+ @result = @open_connection.searchx( args ) {|values|
428
+ yield( values ) if block_given?
429
+ }
430
+ else
431
+ @result = 0
432
+ conn = Connection.new( :host => @host, :port => @port )
433
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
434
+ @result = conn.searchx( args ) {|values|
435
+ yield( values ) if block_given?
436
+ }
437
+ end
438
+ conn.close
439
+ end
440
+
441
+ @result == 0
442
+ end
443
+
444
+ # Searches the LDAP directory for directory entries.
445
+ # Takes a hash argument with parameters. Supported parameters include:
446
+ # * :base (a string specifying the tree-base for the search);
447
+ # * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*);
448
+ # * :attributes (a string or array of strings specifying the LDAP attributes to return from the server);
449
+ # * :return_result (a boolean specifying whether to return a result set).
450
+ # * :attributes_only (a boolean flag, defaults false)
451
+ # * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
452
+ #
453
+ # #search queries the LDAP server and passes <i>each entry</i> to the
454
+ # caller-supplied block, as an object of type Net::LDAP::Entry.
455
+ # If the search returns 1000 entries, the block will
456
+ # be called 1000 times. If the search returns no entries, the block will
457
+ # not be called.
458
+ #
459
+ # #search returns either a result-set or a boolean, depending on the
460
+ # value of the <tt>:return_result</tt> argument. The default behavior is to return
461
+ # a result set, which is a hash. Each key in the hash is a string specifying
462
+ # the DN of an entry. The corresponding value for each key is a Net::LDAP::Entry object.
463
+ # If you request a result set and #search fails with an error, it will return nil.
464
+ # Call #get_operation_result to get the error information returned by
465
+ # the LDAP server.
466
+ #
467
+ # When <tt>:return_result => false,</tt> #search will
468
+ # return only a Boolean, to indicate whether the operation succeeded. This can improve performance
469
+ # with very large result sets, because the library can discard each entry from memory after
470
+ # your block processes it.
471
+ #
472
+ #
473
+ # treebase = "dc=example,dc=com"
474
+ # filter = Net::LDAP::Filter.eq( "mail", "a*.com" )
475
+ # attrs = ["mail", "cn", "sn", "objectclass"]
476
+ # ldap.search( :base => treebase, :filter => filter, :attributes => attrs, :return_result => false ) do |entry|
477
+ # puts "DN: #{entry.dn}"
478
+ # entry.each do |attr, values|
479
+ # puts ".......#{attr}:"
480
+ # values.each do |value|
481
+ # puts " #{value}"
482
+ # end
483
+ # end
484
+ # end
485
+ #
486
+ #--
487
+ # This is a re-implementation of search that replaces the
488
+ # original one (now renamed searchx and possibly destined to go away).
489
+ # The difference is that we return a dataset (or nil) from the
490
+ # call, and pass _each entry_ as it is received from the server
491
+ # to the caller-supplied block. This will probably make things
492
+ # far faster as we can do useful work during the network latency
493
+ # of the search. The downside is that we have no access to the
494
+ # whole set while processing the blocks, so we can't do stuff
495
+ # like sort the DNs until after the call completes.
496
+ # It's also possible that this interacts badly with server timeouts.
497
+ # We'll have to ensure that something reasonable happens if
498
+ # the caller has processed half a result set when we throw a timeout
499
+ # error.
500
+ # Another important difference is that we return a result set from
501
+ # this method rather than a T/F indication.
502
+ # Since this can be very heavy-weight, we define an argument flag
503
+ # that the caller can set to suppress the return of a result set,
504
+ # if he's planning to process every entry as it comes from the server.
505
+ #
506
+ def search args
507
+ result_set = (args and args[:return_result] == false) ? nil : {}
508
+
509
+ if @open_connection
510
+ @result = @open_connection.search( args ) {|entry|
511
+ result_set[entry.dn] = entry if result_set
512
+ yield( entry ) if block_given?
513
+ }
514
+ else
515
+ @result = 0
516
+ conn = Connection.new( :host => @host, :port => @port )
517
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
518
+ @result = conn.search( args ) {|entry|
519
+ (result_set[entry.dn] = entry) if result_set
520
+ yield( entry ) if block_given?
521
+ }
522
+ end
523
+ conn.close
524
+ end
525
+
526
+ @result == 0 and result_set
527
+ end
528
+
529
+ # #bind connects to the LDAP server and requests authentication
530
+ # based on the <tt>:auth</tt> parameter passed to #open or #new.
531
+ # It takes no parameters.
532
+ # User code generally will not call #bind. It will be called
533
+ # implicitly by the library whenever an LDAP operation is
534
+ # requested. #bind can be useful to test authentication.
535
+ #--
536
+ # If there is an @open_connection, then perform the bind
537
+ # on it. Otherwise, connect, bind, and disconnect.
538
+ # The latter operation is obviously useful only as an auth check.
539
+ #
540
+ def bind
541
+ if @open_connection
542
+ @result = @open_connection.bind @auth
543
+ else
544
+ conn = Connection.new( :host => @host, :port => @port )
545
+ @result = conn.bind @auth
546
+ conn.close
547
+ end
548
+
549
+ @result == 0
550
+ end
551
+
552
+ #
553
+ # #bind_as is for testing authentication credentials.
554
+ # Most likely a "standard" name (like a CN or an email
555
+ # address) will be presented along with a password.
556
+ # We'll bind with the main credential given in the
557
+ # constructor, query the full DN of the user given
558
+ # to us as a parameter, then unbind and rebind as the
559
+ # new user.
560
+ #
561
+ # <i>This method is currently an unimplemented stub.</i>
562
+ #
563
+ def bind_as
564
+ end
565
+
566
+ # Adds a new entry to the remote LDAP server.
567
+ # Supported arguments:
568
+ # :dn :: Full DN of the new entry
569
+ # :attributes :: Attributes of the new entry.
570
+ #
571
+ # The attributes argument is supplied as a Hash keyed by Strings or Symbols
572
+ # giving the attribute name, and mapping to Strings or Arrays of Strings
573
+ # giving the actual attribute values. Observe that most LDAP directories
574
+ # enforce schema constraints on the attributes contained in entries.
575
+ # #add will fail with a server-generated error if your attributes violate
576
+ # the server-specific constraints.
577
+ # Here's an example:
578
+ #
579
+ # dn = "cn=George Smith,ou=people,dc=example,dc=com"
580
+ # attr = {
581
+ # :cn => "George Smith",
582
+ # :objectclass => ["top", "inetorgperson"],
583
+ # :sn => "Smith",
584
+ # :mail => "gsmith@example.com"
585
+ # }
586
+ # Net::LDAP.open (:host => host) do |ldap|
587
+ # ldap.add( :dn => dn, :attributes => attr )
588
+ # end
589
+ #
590
+ def add args
591
+ if @open_connection
592
+ @result = @open_connection.add( args )
593
+ else
594
+ @result = 0
595
+ conn = Connection.new( :host => @host, :port => @port )
596
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
597
+ @result = conn.add( args )
598
+ end
599
+ conn.close
600
+ end
601
+ @result == 0
602
+ end
603
+
604
+
605
+ # _DEPRECATED_ - Please use #add_attribute, #replace_attribute, or #delete_attribute.
606
+ #
607
+ # Modifies the attribute values of a particular entry on the LDAP directory.
608
+ # Takes a hash with arguments. Supported arguments are:
609
+ # :dn :: (the full DN of the entry whose attributes are to be modified)
610
+ # :operations :: (the modifications to be performed, detailed next)
611
+ #
612
+ # This method returns True or False to indicate whether the operation
613
+ # succeeded or failed, with extended information available by calling
614
+ # #get_operation_result.
615
+ #
616
+ # The LDAP protocol provides a full and well thought-out set of operations
617
+ # for changing the values of attributes, but they are necessarily somewhat complex
618
+ # and not always intuitive. If these instructions are confusing or incomplete,
619
+ # please send us email or create a bug report on rubyforge.
620
+ #
621
+ # The :operations parameter to #modify takes an array of operation-descriptors.
622
+ # Each individual operation is specified in one element of the array, and
623
+ # most LDAP servers will attempt to perform the operations in order.
624
+ #
625
+ # Each of the operations appearing in the Array must itself be an Array
626
+ # with exactly three elements:
627
+ # an operator:: must be :add, :replace, or :delete
628
+ # an attribute name:: the attribute name (string or symbol) to modify
629
+ # a value:: either a string or an array of strings.
630
+ #
631
+ # The :add operator will, unsurprisingly, add the specified values to
632
+ # the specified attribute. If the attribute does not already exist,
633
+ # :add will create it. Most LDAP servers will generate an error if you
634
+ # to add a value that already exists.
635
+ #
636
+ # :replace will erase the current value(s) for the specified attribute,
637
+ # if there are any, and replace them with the specified value(s).
638
+ #
639
+ # :delete will remove the specified value(s) from the specified attribute.
640
+ # If you pass nil, an empty string, or an empty array as the value parameter
641
+ # to a :delete operation, the _entire_ _attribute_ will be deleted.
642
+ #
643
+ # For example:
644
+ #
645
+ # dn = "mail=modifyme@example.com,ou=people,dc=example,dc=com"
646
+ # ops = [
647
+ # [:add, :mail, "aliasaddress@example.com"],
648
+ # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
649
+ # [:delete, :sn, nil]
650
+ # ]
651
+ # ldap.modify :dn => dn, :operations => ops
652
+ #
653
+ # <i>(This example is contrived since you probably wouldn't add a mail
654
+ # value right before replacing the whole attribute, but it shows that order
655
+ # of execution matters. Also, many LDAP servers won't let you delete SN
656
+ # because it would be a schema violation.)</i>
657
+ #
658
+ # It's essential to keep in mind that if you specify more than one operation in
659
+ # a call to #modify, most LDAP servers will attempt to perform all of the operations
660
+ # in the order you gave them.
661
+ # This matters because you may specify operations on the
662
+ # same attribute which must be performed in a certain order.
663
+ # Most LDAP servers will _stop_ processing your modifications if one of them
664
+ # causes an error on the server (such as a schema-constraint violation).
665
+ # If this happens, you will probably get a result code from the server that
666
+ # reflects only the operation that failed, and you may or may not get extended
667
+ # information that will tell you which one failed. #modify has no notion
668
+ # of an atomic transaction. If you specify a chain of modifications in one
669
+ # call to #modify, and one of them fails, the preceding ones will usually
670
+ # not be "rolled back," resulting in a partial update. This is a limitation
671
+ # of the LDAP protocol, not of Net::LDAP.
672
+ #
673
+ #
674
+ def modify args
675
+ if @open_connection
676
+ @result = @open_connection.modify( args )
677
+ else
678
+ @result = 0
679
+ conn = Connection.new( :host => @host, :port => @port )
680
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
681
+ @result = conn.modify( args )
682
+ end
683
+ conn.close
684
+ end
685
+ @result == 0
686
+ end
687
+
688
+
689
+ # Add a value to an attribute.
690
+ # Takes the full DN of the entry to modify,
691
+ # the name (Symbol or String) of the attribute, and the value (String or
692
+ # Array). If the attribute does not exist (and there are no schema violations),
693
+ # #add_attribute will create it with the caller-specified values.
694
+ # If the attribute already exists (and there are no schema violations), the
695
+ # caller-specified values will be _added_ to the values already present.
696
+ #
697
+ # Returns True or False to indicate whether the operation
698
+ # succeeded or failed, with extended information available by calling
699
+ # #get_operation_result. See also #replace_attribute and #delete_attribute.
700
+ #
701
+ # dn = "cn=modifyme,dc=example,dc=com"
702
+ # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
703
+ #
704
+ def add_attribute dn, attribute, value
705
+ modify :dn => dn, :operations => [[:add, attribute, value]]
706
+ end
707
+
708
+ # Replace the value of an attribute.
709
+ # #replace_attribute can be thought of as equivalent to calling #delete_attribute
710
+ # followed by #add_attribute. It takes the full DN of the entry to modify,
711
+ # the name (Symbol or String) of the attribute, and the value (String or
712
+ # Array). If the attribute does not exist, it will be created with the
713
+ # caller-specified value(s). If the attribute does exist, its values will be
714
+ # _discarded_ and replaced with the caller-specified values.
715
+ #
716
+ # Returns True or False to indicate whether the operation
717
+ # succeeded or failed, with extended information available by calling
718
+ # #get_operation_result. See also #add_attribute and #delete_attribute.
719
+ #
720
+ # dn = "cn=modifyme,dc=example,dc=com"
721
+ # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
722
+ #
723
+ def replace_attribute dn, attribute, value
724
+ modify :dn => dn, :operations => [[:replace, attribute, value]]
725
+ end
726
+
727
+ # Delete an attribute and all its values.
728
+ # Takes the full DN of the entry to modify, and the
729
+ # name (Symbol or String) of the attribute to delete.
730
+ #
731
+ # Returns True or False to indicate whether the operation
732
+ # succeeded or failed, with extended information available by calling
733
+ # #get_operation_result. See also #add_attribute and #replace_attribute.
734
+ #
735
+ # dn = "cn=modifyme,dc=example,dc=com"
736
+ # ldap.delete_attribute dn, :mail
737
+ #
738
+ def delete_attribute dn, attribute
739
+ modify :dn => dn, :operations => [[:delete, attribute, nil]]
740
+ end
741
+
742
+
743
+ # Rename an entry on the remote DIS by changing the last RDN of its DN.
744
+ # _Documentation_ _stub_
745
+ #
746
+ def rename args
747
+ if @open_connection
748
+ @result = @open_connection.rename( args )
749
+ else
750
+ @result = 0
751
+ conn = Connection.new( :host => @host, :port => @port )
752
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
753
+ @result = conn.rename( args )
754
+ end
755
+ conn.close
756
+ end
757
+ @result == 0
758
+ end
759
+
760
+ # modify_rdn is an alias for #rename.
761
+ def modify_rdn args
762
+ rename args
763
+ end
764
+
765
+ # Delete an entry from the LDAP directory.
766
+ # Takes a hash of arguments.
767
+ # The only supported argument is :dn, which must
768
+ # give the complete DN of the entry to be deleted.
769
+ # Returns True or False to indicate whether the delete
770
+ # succeeded. Extended status information is available by
771
+ # calling #get_operation_result.
772
+ #
773
+ # dn = "mail=deleteme@example.com,ou=people,dc=example,dc=com"
774
+ # ldap.delete :dn => dn
775
+ #
776
+ def delete args
777
+ if @open_connection
778
+ @result = @open_connection.delete( args )
779
+ else
780
+ @result = 0
781
+ conn = Connection.new( :host => @host, :port => @port )
782
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
783
+ @result = conn.delete( args )
784
+ end
785
+ conn.close
786
+ end
787
+ @result == 0
788
+ end
789
+
790
+ end # class LDAP
791
+
792
+
793
+
794
+ class LDAP
795
+ # This is a private class used internally by the library. It should not be called by user code.
796
+ class Connection # :nodoc:
797
+
798
+ LdapVersion = 3
799
+
800
+
801
+ #--
802
+ # initialize
803
+ #
804
+ def initialize server
805
+ begin
806
+ @conn = TCPsocket.new( server[:host], server[:port] )
807
+ rescue
808
+ raise LdapError.new( "no connection to server" )
809
+ end
810
+
811
+ yield self if block_given?
812
+ end
813
+
814
+
815
+ #--
816
+ # close
817
+ # This is provided as a convenience method to make
818
+ # sure a connection object gets closed without waiting
819
+ # for a GC to happen. Clients shouldn't have to call it,
820
+ # but perhaps it will come in handy someday.
821
+ def close
822
+ @conn.close
823
+ @conn = nil
824
+ end
825
+
826
+ #--
827
+ # next_msgid
828
+ #
829
+ def next_msgid
830
+ @msgid ||= 0
831
+ @msgid += 1
832
+ end
833
+
834
+
835
+ #--
836
+ # bind
837
+ #
838
+ def bind auth
839
+ user,psw = case auth[:method]
840
+ when :anonymous
841
+ ["",""]
842
+ when :simple
843
+ [auth[:username] || auth[:dn], auth[:password]]
844
+ end
845
+ raise LdapError.new( "invalid binding information" ) unless (user && psw)
846
+
847
+ msgid = next_msgid.to_ber
848
+ request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
849
+ request_pkt = [msgid, request].to_ber_sequence
850
+ @conn.write request_pkt
851
+
852
+ (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
853
+ pdu.result_code
854
+ end
855
+
856
+ #--
857
+ # search
858
+ # Alternate implementation, this yields each search entry to the caller
859
+ # as it are received.
860
+ # TODO, certain search parameters are hardcoded.
861
+ # TODO, if we mis-parse the server results or the results are wrong, we can block
862
+ # forever. That's because we keep reading results until we get a type-5 packet,
863
+ # which might never come. We need to support the time-limit in the protocol.
864
+ #--
865
+ # WARNING: this code substantially recapitulates the searchx method.
866
+ #
867
+ def search args = {}
868
+ search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
869
+ search_base = (args && args[:base]) || "dc=example,dc=com"
870
+ search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
871
+
872
+ attributes_only = (args and args[:attributes_only] == true)
873
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
874
+ raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
875
+
876
+ request = [
877
+ search_base.to_ber,
878
+ scope.to_ber_enumerated,
879
+ 0.to_ber_enumerated,
880
+ 0.to_ber,
881
+ 0.to_ber,
882
+ attributes_only.to_ber,
883
+ search_filter.to_ber,
884
+ search_attributes.to_ber_sequence
885
+ ].to_ber_appsequence(3)
886
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
887
+ @conn.write pkt
888
+
889
+ result_code = 0
890
+
891
+ while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
892
+ case pdu.app_tag
893
+ when 4 # search-data
894
+ yield( pdu.search_entry ) if block_given?
895
+ when 5 # search-result
896
+ result_code = pdu.result_code
897
+ break
898
+ else
899
+ raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
900
+ end
901
+ end
902
+
903
+ result_code
904
+ end
905
+
906
+
907
+ #--
908
+ # searchx
909
+ # Original implementation, this doesn't return until all data have been
910
+ # received from the server.
911
+ # TODO, certain search parameters are hardcoded.
912
+ # TODO, if we mis-parse the server results or the results are wrong, we can block
913
+ # forever. That's because we keep reading results until we get a type-5 packet,
914
+ # which might never come. We need to support the time-limit in the protocol.
915
+ #--
916
+ # WARNING: this code substantially recapitulates the search method.
917
+ #
918
+ def searchx args
919
+ search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
920
+ search_base = (args && args[:base]) || "dc=example,dc=com"
921
+ search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
922
+ request = [
923
+ search_base.to_ber,
924
+ 2.to_ber_enumerated,
925
+ 0.to_ber_enumerated,
926
+ 0.to_ber,
927
+ 0.to_ber,
928
+ false.to_ber,
929
+ search_filter.to_ber,
930
+ search_attributes.to_ber_sequence
931
+ ].to_ber_appsequence(3)
932
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
933
+ @conn.write pkt
934
+
935
+ search_results = {}
936
+ result_code = 0
937
+
938
+ while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
939
+ case pdu.app_tag
940
+ when 4 # search-data
941
+ search_results [pdu.search_dn] = pdu.search_attributes
942
+ when 5 # search-result
943
+ result_code = pdu.result_code
944
+ block_given? and yield( search_results )
945
+ break
946
+ else
947
+ raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
948
+ end
949
+ end
950
+
951
+ result_code
952
+ end
953
+
954
+ #--
955
+ # modify
956
+ # TODO, need to support a time limit, in case the server fails to respond.
957
+ # TODO!!! We're throwing an exception here on empty DN.
958
+ # Should return a proper error instead, probaby from farther up the chain.
959
+ # TODO!!! If the user specifies a bogus opcode, we'll throw a
960
+ # confusing error here ("to_ber_enumerated is not defined on nil").
961
+ #
962
+ def modify args
963
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
964
+ modify_ops = []
965
+ a = args[:operations] and a.each {|op, attr, values|
966
+ # TODO, fix the following line, which gives a bogus error
967
+ # if the opcode is invalid.
968
+ op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
969
+ 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
970
+ }
971
+
972
+ request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
973
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
974
+ @conn.write pkt
975
+
976
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
977
+ pdu.result_code
978
+ end
979
+
980
+
981
+ #--
982
+ # add
983
+ # TODO, need to support a time limit, in case the server fails to respond.
984
+ #
985
+ def add args
986
+ add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
987
+ add_attrs = []
988
+ a = args[:attributes] and a.each {|k,v|
989
+ add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
990
+ }
991
+
992
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
993
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
994
+ @conn.write pkt
995
+
996
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
997
+ pdu.result_code
998
+ end
999
+
1000
+
1001
+ #--
1002
+ # rename
1003
+ # TODO, need to support a time limit, in case the server fails to respond.
1004
+ #
1005
+ def rename args
1006
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
1007
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1008
+ delete_attrs = args[:delete_attributes] ? true : false
1009
+
1010
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
1011
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1012
+ @conn.write pkt
1013
+
1014
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
1015
+ pdu.result_code
1016
+ end
1017
+
1018
+
1019
+ #--
1020
+ # delete
1021
+ # TODO, need to support a time limit, in case the server fails to respond.
1022
+ #
1023
+ def delete args
1024
+ dn = args[:dn] or raise "Unable to delete empty DN"
1025
+
1026
+ request = dn.to_s.to_ber_application_string(10)
1027
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1028
+ @conn.write pkt
1029
+
1030
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" )
1031
+ pdu.result_code
1032
+ end
1033
+
1034
+
1035
+ end # class Connection
1036
+ end # class LDAP
1037
+
1038
+
1039
+ end # module Net
1040
+
1041
+