ruby-net-ldap 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ChangeLog CHANGED
@@ -1,5 +1,19 @@
1
1
  = Net::LDAP Changelog
2
2
 
3
+ == Net::LDAP 0.0.2: July 12, 2006
4
+ * Fixed malformation in distro tarball and gem.
5
+ * Improved documentation.
6
+ * Supported "paged search control."
7
+ * Added a range of API improvements.
8
+ * Thanks to Andre Nathan, andre@digirati.com.br, for valuable
9
+ suggestions.
10
+ * Added support for LE and GE search filters.
11
+ * Added support for Search referrals.
12
+ * Fixed a regression with openldap 2.2.x and higher caused
13
+ by the introduction of RFC-2696 controls. Thanks to Andre
14
+ Nathan for reporting the problem.
15
+ * Added support for RFC-2254 filter syntax.
16
+
3
17
  == Net::LDAP 0.0.1: May 1, 2006
4
18
  * Initial release.
5
19
  * Client functionality is near-complete, although the APIs
data/README CHANGED
@@ -3,19 +3,22 @@ Net::LDAP is an LDAP support library written in pure Ruby. It supports all
3
3
  LDAP client features, and a subset of server features as well.
4
4
 
5
5
  Homepage:: http://rubyforge.org/projects/net-ldap/
6
- Copyright:: 2006 by Francis Cianfrocca
6
+ Copyright:: (C) 2006 by Francis Cianfrocca
7
7
 
8
8
  Original developer: Francis Cianfrocca
9
9
  Contributions by Austin Ziegler gratefully acknowledged.
10
10
 
11
11
  == LICENCE NOTES
12
12
  Please read the file LICENCE for licensing restrictions on this library. In
13
- it simplest terms, this library is available under the same terms as Ruby
13
+ the simplest terms, this library is available under the same terms as Ruby
14
14
  itself.
15
15
 
16
16
  == Requirements
17
17
  Net::LDAP requires Ruby 1.8.2 or better.
18
18
 
19
+ == Documentation
20
+ See Net::LDAP for documentation and usage samples.
21
+
19
22
  #--
20
23
  # Net::LDAP for Ruby.
21
24
  # http://rubyforge.org/projects/net-ldap/
@@ -24,6 +27,6 @@ Net::LDAP requires Ruby 1.8.2 or better.
24
27
  # Available under the same terms as Ruby. See LICENCE in the main
25
28
  # distribution for full licensing information.
26
29
  #
27
- # $Id: README 82 2006-04-30 11:36:18Z blackhedd $
30
+ # $Id: README 141 2006-07-12 10:37:37Z blackhedd $
28
31
  #++
29
32
  # vim: sts=2 sw=2 ts=4 et ai tw=77
@@ -1,4 +1,4 @@
1
- # $Id: ldap.rb 94 2006-05-01 07:19:12Z blackhedd $
1
+ # $Id: ldap.rb 141 2006-07-12 10:37:37Z blackhedd $
2
2
  #
3
3
  # Net::LDAP for Ruby
4
4
  #
@@ -32,7 +32,7 @@ module Net
32
32
  # == Net::LDAP
33
33
  #
34
34
  # This library provides a pure-Ruby implementation of the
35
- # LDAP client protocol, per RFC-1777.
35
+ # LDAP client protocol, per RFC-2251.
36
36
  # It can be used to access any server which implements the
37
37
  # LDAP protocol.
38
38
  #
@@ -41,7 +41,25 @@ module Net
41
41
  # the LDAP protocol itself, and thus presenting as Ruby-like
42
42
  # a programming interface as possible.
43
43
  #
44
- # === Quick-start for the Impatient
44
+ # == Quick-start for the Impatient
45
+ # === Quick Example of a user-authentication against an LDAP directory:
46
+ #
47
+ # require 'rubygems'
48
+ # require 'net/ldap'
49
+ #
50
+ # ldap = Net::LDAP.new
51
+ # ldap.host = your_server_ip_address
52
+ # ldap.port = 389
53
+ # ldap.auth "joe_user", "opensesame"
54
+ # if ldap.bind
55
+ # # authentication succeeded
56
+ # else
57
+ # # authentication failed
58
+ # end
59
+ #
60
+ #
61
+ # === Quick Example of a search against an LDAP directory:
62
+ #
45
63
  # require 'rubygems'
46
64
  # require 'net/ldap'
47
65
  #
@@ -69,14 +87,14 @@ module Net
69
87
  # p ldap.get_operation_result
70
88
  #
71
89
  #
72
- # == Quick introduction to LDAP
90
+ # == A Brief Introduction to LDAP
73
91
  #
74
- # We're going to provide a quick and highly informal introduction to LDAP
92
+ # We're going to provide a quick, informal introduction to LDAP
75
93
  # terminology and
76
94
  # typical operations. If you're comfortable with this material, skip
77
95
  # ahead to "How to use Net::LDAP." If you want a more rigorous treatment
78
96
  # of this material, we recommend you start with the various IETF and ITU
79
- # standards that control LDAP.
97
+ # standards that relate to LDAP.
80
98
  #
81
99
  # === Entities
82
100
  # LDAP is an Internet-standard protocol used to access directory servers.
@@ -116,17 +134,17 @@ module Net
116
134
  # range of attributes, and constrain their values according to standard
117
135
  # rules.
118
136
  #
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
137
+ # A good example of an attribute is <tt>sn,</tt> which stands for "Surname."
138
+ # This attribute is generally used to store a person's surname, or last name.
139
+ # Most directories enforce the standard convention that
140
+ # an entity's <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP
141
+ # jargon, that means that <tt>sn</tt> must be <i>present</i> and
124
142
  # <i>single-valued.</i>
125
143
  #
126
144
  # Another attribute is <tt>mail,</tt> which is used to store email addresses.
127
145
  # (No, there is no attribute called "email," perhaps because X.400 terminology
128
146
  # 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
147
+ # from <tt>sn</tt> in that most directories permit any number of values for the
130
148
  # <tt>mail</tt> attribute, including zero.
131
149
  #
132
150
  #
@@ -173,7 +191,7 @@ module Net
173
191
  # set of attribute values for each entity, depending on what attributes the search requested.
174
192
  #
175
193
  # ==== Add
176
- # #add operation specifies a new DN and an initial set of attribute values. If the operation
194
+ # #add specifies a new DN and an initial set of attribute values. If the operation
177
195
  # succeeds, a new entity with the corresponding DN and attributes is added to the directory.
178
196
  #
179
197
  # ==== Modify
@@ -181,11 +199,11 @@ module Net
181
199
  # the attribute values stored in the directory for a particular entity.
182
200
  # #modify may add or delete attributes (which are lists of values) or it change attributes by
183
201
  # adding to or deleting from their values.
184
- # There are three easier methods to modify an entry's attribute values:
202
+ # Net::LDAP provides three easier methods to modify an entry's attribute values:
185
203
  # #add_attribute, #replace_attribute, and #delete_attribute.
186
204
  #
187
205
  # ==== Delete
188
- # #delete operation specifies an entity DN. If it succeeds, the entity and all its attributes
206
+ # #delete specifies an entity DN. If it succeeds, the entity and all its attributes
189
207
  # is removed from the directory.
190
208
  #
191
209
  # ==== Rename (or Modify RDN)
@@ -238,7 +256,7 @@ module Net
238
256
 
239
257
  class LdapError < Exception; end
240
258
 
241
- VERSION = "0.0.1"
259
+ VERSION = "0.0.2"
242
260
 
243
261
 
244
262
  SearchScope_BaseObject = 0
@@ -266,6 +284,7 @@ module Net
266
284
  14 => :array, # CompareRequest
267
285
  15 => :array, # CompareResponse
268
286
  16 => :array, # AbandonRequest
287
+ 19 => :array, # SearchResultReferral
269
288
  24 => :array, # Unsolicited Notification
270
289
  }
271
290
  },
@@ -274,6 +293,10 @@ module Net
274
293
  0 => :string, # password
275
294
  1 => :string, # Kerberos v4
276
295
  2 => :string, # Kerberos v5
296
+ },
297
+ :constructed => {
298
+ 0 => :array, # RFC-2251 Control
299
+ 3 => :array, # Seach referral
277
300
  }
278
301
  }
279
302
  }
@@ -281,12 +304,16 @@ module Net
281
304
  DefaultHost = "127.0.0.1"
282
305
  DefaultPort = 389
283
306
  DefaultAuth = {:method => :anonymous}
307
+ DefaultTreebase = "dc=com"
284
308
 
285
309
 
286
310
  ResultStrings = {
287
311
  0 => "Success",
288
312
  1 => "Operations Error",
289
313
  2 => "Protocol Error",
314
+ 3 => "Time Limit Exceeded",
315
+ 4 => "Size Limit Exceeded",
316
+ 12 => "Unavailable crtical extension",
290
317
  16 => "No Such Attribute",
291
318
  17 => "Undefined Attribute Type",
292
319
  20 => "Attribute or Value Exists",
@@ -303,13 +330,23 @@ module Net
303
330
  68 => "Entry Already Exists"
304
331
  }
305
332
 
333
+
334
+ module LdapControls
335
+ PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
336
+ end
337
+
338
+
306
339
  #
307
340
  # LDAP::result2string
308
341
  #
309
- def LDAP::result2string code
342
+ def LDAP::result2string code # :nodoc:
310
343
  ResultStrings[code] || "unknown result (#{code})"
311
344
  end
312
345
 
346
+
347
+ attr_accessor :host, :port, :base
348
+
349
+
313
350
  # Instantiate an object of type Net::LDAP to perform directory operations.
314
351
  # This constructor takes a Hash containing arguments. The following arguments
315
352
  # are supported:
@@ -318,6 +355,7 @@ module Net
318
355
  # * :auth => a Hash containing authorization parameters. Currently supported values include:
319
356
  # {:method => :anonymous} and
320
357
  # {:method => :simple, :username => your_user_name, :password => your_password }
358
+ # The password parameter may be a Proc that returns a String.
321
359
  #
322
360
  # Instantiating a Net::LDAP object does <i>not</i> result in network traffic to
323
361
  # the LDAP server. It simply stores the connection and binding parameters in the
@@ -328,6 +366,11 @@ module Net
328
366
  @port = args[:port] || DefaultPort
329
367
  @verbose = false # Make this configurable with a switch on the class.
330
368
  @auth = args[:auth] || DefaultAuth
369
+ @base = args[:base] || DefaultTreebase
370
+
371
+ if pr = @auth[:password] and pr.respond_to?(:call)
372
+ @auth[:password] = pr.call
373
+ end
331
374
 
332
375
  # This variable is only set when we are created with LDAP::open.
333
376
  # All of our internal methods will connect using it, or else
@@ -335,6 +378,45 @@ module Net
335
378
  @open_connection = nil
336
379
  end
337
380
 
381
+ # Convenience method to specify authentication credentials to the LDAP
382
+ # server. Currently supports simple authentication requiring
383
+ # a username and password.
384
+ #
385
+ # Observe that on most LDAP servers,
386
+ # the username is a complete DN. However, with A/D, it's often possible
387
+ # to give only a user-name rather than a complete DN. In the latter
388
+ # case, beware that many A/D servers are configured to permit anonymous
389
+ # (uncredentialled) binding, and will silently accept your binding
390
+ # as anonymous if you give an unrecognized username. This is not usually
391
+ # what you want. (See #get_operation_result.)
392
+ #
393
+ # <b>Important:</b> The password argument may be a Proc that returns a string.
394
+ # This makes it possible for you to write client programs that solicit
395
+ # passwords from users or from other data sources without showing them
396
+ # in your code or on command lines.
397
+ #
398
+ # require 'net/ldap'
399
+ #
400
+ # ldap = Net::LDAP.new
401
+ # ldap.host = server_ip_address
402
+ # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw"
403
+ #
404
+ # Alternatively (with a password block):
405
+ #
406
+ # require 'net/ldap'
407
+ #
408
+ # ldap = Net::LDAP.new
409
+ # ldap.host = server_ip_address
410
+ # psw = proc { your_psw_function }
411
+ # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", psw
412
+ #
413
+ def authenticate username, password
414
+ password = password.call if password.respond_to?(:call)
415
+ @auth = {:method => :simple, :username => username, :password => password}
416
+ end
417
+
418
+ alias_method :auth, :authenticate
419
+
338
420
  # #open takes the same parameters as #new. #open makes a network connection to the
339
421
  # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
340
422
  # Within the block, you can call any of the instance methods of Net::LDAP to
@@ -409,38 +491,6 @@ module Net
409
491
  end
410
492
 
411
493
 
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
494
  # Searches the LDAP directory for directory entries.
445
495
  # Takes a hash argument with parameters. Supported parameters include:
446
496
  # * :base (a string specifying the tree-base for the search);
@@ -456,6 +506,8 @@ module Net
456
506
  # be called 1000 times. If the search returns no entries, the block will
457
507
  # not be called.
458
508
  #
509
+ #--
510
+ # ORIGINAL TEXT, replaced 04May06.
459
511
  # #search returns either a result-set or a boolean, depending on the
460
512
  # value of the <tt>:return_result</tt> argument. The default behavior is to return
461
513
  # a result set, which is a hash. Each key in the hash is a string specifying
@@ -463,6 +515,13 @@ module Net
463
515
  # If you request a result set and #search fails with an error, it will return nil.
464
516
  # Call #get_operation_result to get the error information returned by
465
517
  # the LDAP server.
518
+ #++
519
+ # #search returns either a result-set or a boolean, depending on the
520
+ # value of the <tt>:return_result</tt> argument. The default behavior is to return
521
+ # a result set, which is an Array of objects of class Net::LDAP::Entry.
522
+ # If you request a result set and #search fails with an error, it will return nil.
523
+ # Call #get_operation_result to get the error information returned by
524
+ # the LDAP server.
466
525
  #
467
526
  # When <tt>:return_result => false,</tt> #search will
468
527
  # return only a Boolean, to indicate whether the operation succeeded. This can improve performance
@@ -503,12 +562,19 @@ module Net
503
562
  # that the caller can set to suppress the return of a result set,
504
563
  # if he's planning to process every entry as it comes from the server.
505
564
  #
506
- def search args
507
- result_set = (args and args[:return_result] == false) ? nil : {}
565
+ # REINTERPRETED the result set, 04May06. Originally this was a hash
566
+ # of entries keyed by DNs. But let's get away from making users
567
+ # handle DNs. Change it to a plain array. Eventually we may
568
+ # want to return a Dataset object that delegates to an internal
569
+ # array, so we can provide sort methods and what-not.
570
+ #
571
+ def search args = {}
572
+ args[:base] ||= @base
573
+ result_set = (args and args[:return_result] == false) ? nil : []
508
574
 
509
575
  if @open_connection
510
576
  @result = @open_connection.search( args ) {|entry|
511
- result_set[entry.dn] = entry if result_set
577
+ result_set << entry if result_set
512
578
  yield( entry ) if block_given?
513
579
  }
514
580
  else
@@ -516,7 +582,7 @@ module Net
516
582
  conn = Connection.new( :host => @host, :port => @port )
517
583
  if (@result = conn.bind( args[:auth] || @auth )) == 0
518
584
  @result = conn.search( args ) {|entry|
519
- (result_set[entry.dn] = entry) if result_set
585
+ result_set << entry if result_set
520
586
  yield( entry ) if block_given?
521
587
  }
522
588
  end
@@ -526,12 +592,46 @@ module Net
526
592
  @result == 0 and result_set
527
593
  end
528
594
 
529
- # #bind connects to the LDAP server and requests authentication
595
+ # #bind connects to an LDAP server and requests authentication
530
596
  # based on the <tt>:auth</tt> parameter passed to #open or #new.
531
597
  # 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.
598
+ #
599
+ # User code does not need to call #bind directly. It will be called
600
+ # implicitly by the library whenever you invoke an LDAP operation,
601
+ # such as #search or #add.
602
+ #
603
+ # It is useful, however, to call #bind in your own code when the
604
+ # only operation you intend to perform against the directory is
605
+ # to validate a login credential. #bind returns true or false
606
+ # to indicate whether the binding was successful. Reasons for
607
+ # failure include malformed or unrecognized usernames and
608
+ # incorrect passwords. Use #get_operation_result to find out
609
+ # what happened in case of failure.
610
+ #
611
+ # Here's a typical example using #bind to authenticate a
612
+ # credential which was (perhaps) solicited from the user of a
613
+ # web site:
614
+ #
615
+ # require 'net/ldap'
616
+ # ldap = Net::LDAP.new
617
+ # ldap.host = your_server_ip_address
618
+ # ldap.port = 389
619
+ # ldap.auth your_user_name, your_user_password
620
+ # if ldap.bind
621
+ # # authentication succeeded
622
+ # else
623
+ # # authentication failed
624
+ # p ldap.get_operation_result
625
+ # end
626
+ #
627
+ # You don't have to create a new instance of Net::LDAP every time
628
+ # you perform a binding in this way. If you prefer, you can cache the Net::LDAP object
629
+ # and re-use it to perform subsequent bindings, <i>provided</i> you call
630
+ # #auth to specify a new credential before calling #bind. Otherwise, you'll
631
+ # just re-authenticate the previous user! (You don't need to re-set
632
+ # the values of #host and #port.) As noted in the documentation for #auth,
633
+ # the password parameter can be a Ruby Proc instead of a String.
634
+ #
535
635
  #--
536
636
  # If there is an @open_connection, then perform the bind
537
637
  # on it. Otherwise, connect, bind, and disconnect.
@@ -864,92 +964,109 @@ module Net
864
964
  #--
865
965
  # WARNING: this code substantially recapitulates the searchx method.
866
966
  #
967
+ # 02May06: Well, I added support for RFC-2696-style paged searches.
968
+ # This is used on all queries because the extension is marked non-critical.
969
+ # As far as I know, only A/D uses this, but it's required for A/D. Otherwise
970
+ # you won't get more than 1000 results back from a query.
971
+ # This implementation is kindof clunky and should probably be refactored.
972
+ # Also, is it my imagination, or are A/Ds the slowest directory servers ever???
973
+ #
867
974
  def search args = {}
868
975
  search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
869
976
  search_base = (args && args[:base]) || "dc=example,dc=com"
870
977
  search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
978
+ return_referrals = args && args[:return_referrals] == true
871
979
 
872
980
  attributes_only = (args and args[:attributes_only] == true)
873
981
  scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
874
982
  raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
875
983
 
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
-
984
+ # An interesting value for the size limit would be close to A/D's built-in
985
+ # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
986
+ # on anything bigger than 126. You get a silent error that is easily visible
987
+ # by running slapd in debug mode. Go figure.
988
+ rfc2696_cookie = [126, ""]
889
989
  result_code = 0
890
990
 
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}" )
991
+ loop {
992
+ # should collect this into a private helper to clarify the structure
993
+
994
+ request = [
995
+ search_base.to_ber,
996
+ scope.to_ber_enumerated,
997
+ 0.to_ber_enumerated,
998
+ 0.to_ber,
999
+ 0.to_ber,
1000
+ attributes_only.to_ber,
1001
+ search_filter.to_ber,
1002
+ search_attributes.to_ber_sequence
1003
+ ].to_ber_appsequence(3)
1004
+
1005
+ controls = [
1006
+ [
1007
+ LdapControls::PagedResults.to_ber,
1008
+ false.to_ber, # criticality MUST be false to interoperate with normal LDAPs.
1009
+ rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
1010
+ ].to_ber_sequence
1011
+ ].to_ber_contextspecific(0)
1012
+
1013
+ pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
1014
+ @conn.write pkt
1015
+
1016
+ result_code = 0
1017
+ controls = []
1018
+
1019
+ while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
1020
+ case pdu.app_tag
1021
+ when 4 # search-data
1022
+ yield( pdu.search_entry ) if block_given?
1023
+ when 19 # search-referral
1024
+ if return_referrals
1025
+ if block_given?
1026
+ se = Net::LDAP::Entry.new
1027
+ se[:search_referrals] = (pdu.search_referrals || [])
1028
+ yield se
1029
+ end
1030
+ end
1031
+ #p pdu.referrals
1032
+ when 5 # search-result
1033
+ result_code = pdu.result_code
1034
+ controls = pdu.result_controls
1035
+ break
1036
+ else
1037
+ raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
1038
+ end
900
1039
  end
901
- end
902
1040
 
903
- result_code
904
- end
1041
+ # When we get here, we have seen a type-5 response.
1042
+ # If there is no error AND there is an RFC-2696 cookie,
1043
+ # then query again for the next page of results.
1044
+ # If not, we're done.
1045
+ # Don't screw this up or we'll break every search we do.
1046
+ more_pages = false
1047
+ if result_code == 0 and controls
1048
+ controls.each do |c|
1049
+ if c.oid == LdapControls::PagedResults
1050
+ more_pages = false # just in case some bogus server sends us >1 of these.
1051
+ if c.value and c.value.length > 0
1052
+ cookie = c.value.read_ber[1]
1053
+ if cookie and cookie.length > 0
1054
+ rfc2696_cookie[1] = cookie
1055
+ more_pages = true
1056
+ end
1057
+ end
1058
+ end
1059
+ end
1060
+ end
905
1061
 
1062
+ break unless more_pages
1063
+ } # loop
906
1064
 
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
1065
+ result_code
1066
+ end
934
1067
 
935
- search_results = {}
936
- result_code = 0
937
1068
 
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
1069
 
951
- result_code
952
- end
953
1070
 
954
1071
  #--
955
1072
  # modify
@@ -1,4 +1,4 @@
1
- # $Id: entry.rb 95 2006-05-01 12:19:16Z blackhedd $
1
+ # $Id: entry.rb 123 2006-05-18 03:52:38Z blackhedd $
2
2
  #
3
3
  # LDAP Entry (search-result) support classes
4
4
  #
@@ -33,46 +33,129 @@ module Net
33
33
  class LDAP
34
34
 
35
35
 
36
+ # Objects of this class represent individual entries in an LDAP
37
+ # directory. User code generally does not instantiate this class.
38
+ # Net::LDAP#search provides objects of this class to user code,
39
+ # either as block parameters or as return values.
40
+ #
41
+ # In LDAP-land, an "entry" is a collection of attributes that are
42
+ # uniquely and globally identified by a DN ("Distinguished Name").
43
+ # Attributes are identified by short, descriptive words or phrases.
44
+ # Although a directory is
45
+ # free to implement any attribute name, most of them follow rigorous
46
+ # standards so that the range of commonly-encountered attribute
47
+ # names is not large.
48
+ #
49
+ # An attribute name is case-insensitive. Most directories also
50
+ # restrict the range of characters allowed in attribute names.
51
+ # To simplify handling attribute names, Net::LDAP::Entry
52
+ # internally converts them to a standard format. Therefore, the
53
+ # methods which take attribute names can take Strings or Symbols,
54
+ # and work correctly regardless of case or capitalization.
55
+ #
56
+ # An attribute consists of zero or more data items called
57
+ # <i>values.</i> An entry is the combination of a unique DN, a set of attribute
58
+ # names, and a (possibly-empty) array of values for each attribute.
59
+ #
60
+ # Class Net::LDAP::Entry provides convenience methods for dealing
61
+ # with LDAP entries.
62
+ # In addition to the methods documented below, you may access individual
63
+ # attributes of an entry simply by giving the attribute name as
64
+ # the name of a method call. For example:
65
+ # ldap.search( ... ) do |entry|
66
+ # puts "Common name: #{entry.cn}"
67
+ # puts "Email addresses:"
68
+ # entry.mail.each {|ma| puts ma}
69
+ # end
70
+ # If you use this technique to access an attribute that is not present
71
+ # in a particular Entry object, a NoMethodError exception will be raised.
72
+ #
73
+ #--
74
+ # Ugly problem to fix someday: We key off the internal hash with
75
+ # a canonical form of the attribute name: convert to a string,
76
+ # downcase, then take the symbol. Unfortunately we do this in
77
+ # at least three places. Should do it in ONE place.
36
78
  class Entry
37
79
 
38
- def initialize dn = nil
80
+ # This constructor is not generally called by user code.
81
+ def initialize dn = nil # :nodoc:
39
82
  @myhash = Hash.new {|k,v| k[v] = [] }
40
83
  @myhash[:dn] = [dn]
41
84
  end
42
85
 
43
86
 
44
- def []= name, value
87
+ def []= name, value # :nodoc:
45
88
  sym = name.to_s.downcase.intern
46
89
  @myhash[sym] = value
47
90
  end
48
91
 
49
92
 
50
93
  #--
51
- # We have to deal with this one as we do []=
94
+ # We have to deal with this one as we do with []=
52
95
  # because this one and not the other one gets called
53
96
  # in formulations like entry["CN"] << cn.
54
97
  #
55
- def [] name
98
+ def [] name # :nodoc:
56
99
  name = name.to_s.downcase.intern unless name.is_a?(Symbol)
57
100
  @myhash[name]
58
101
  end
59
102
 
103
+ # Returns the dn of the Entry as a String.
60
104
  def dn
61
105
  self[:dn][0]
62
106
  end
63
107
 
108
+ # Returns an array of the attribute names present in the Entry.
64
109
  def attribute_names
65
110
  @myhash.keys
66
111
  end
67
112
 
113
+ # Accesses each of the attributes present in the Entry.
114
+ # Calls a user-supplied block with each attribute in turn,
115
+ # passing two arguments to the block: a Symbol giving
116
+ # the name of the attribute, and a (possibly empty)
117
+ # Array of data values.
118
+ #
68
119
  def each
69
120
  if block_given?
70
- attribute_names.each {|a| yield a, self[a] }
121
+ attribute_names.each {|a|
122
+ attr_name,values = a,self[a]
123
+ yield attr_name, values
124
+ }
71
125
  end
72
126
  end
73
127
 
74
128
  alias_method :each_attribute, :each
75
129
 
130
+
131
+ #--
132
+ # Convenience method to convert unknown method names
133
+ # to attribute references. Of course the method name
134
+ # comes to us as a symbol, so let's save a little time
135
+ # and not bother with the to_s.downcase two-step.
136
+ # Of course that means that a method name like mAIL
137
+ # won't work, but we shouldn't be encouraging that
138
+ # kind of bad behavior in the first place.
139
+ # Maybe we should thow something if the caller sends
140
+ # arguments or a block...
141
+ #
142
+ def method_missing *args, &block # :nodoc:
143
+ s = args[0].to_s.downcase.intern
144
+ if attribute_names.include?(s)
145
+ self[s]
146
+ elsif s.to_s[-1] == 61 and s.to_s.length > 1
147
+ value = args[1] or raise RuntimeError.new( "unable to set value" )
148
+ value = [value] unless value.is_a?(Array)
149
+ name = s.to_s[0..-2].intern
150
+ self[name] = value
151
+ else
152
+ raise NoMethodError.new( "undefined method '#{s}'" )
153
+ end
154
+ end
155
+
156
+ def write
157
+ end
158
+
76
159
  end # class Entry
77
160
 
78
161
 
@@ -1,4 +1,4 @@
1
- # $Id: filter.rb 78 2006-04-26 02:57:34Z blackhedd $
1
+ # $Id: filter.rb 132 2006-06-27 17:35:18Z blackhedd $
2
2
  #
3
3
  #
4
4
  #----------------------------------------------------------------------------
@@ -75,11 +75,13 @@ class Filter
75
75
  # This example selects any entry with a <tt>mail</tt> value containing
76
76
  # the substring "anderson":
77
77
  # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
78
+ #--
79
+ # Removed gt and lt. They ain't in the standard!
78
80
  #
79
81
  def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
80
82
  def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
81
- def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
82
- def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
83
+ #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
84
+ #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
83
85
  def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
84
86
  def Filter::le attribute, value; Filter.new :le, attribute, value; end
85
87
 
@@ -110,6 +112,7 @@ class Filter
110
112
  #
111
113
  #--
112
114
  # This operator can't be !, evidently. Try it.
115
+ # Removed GT and LT. They're not in the RFC.
113
116
  def ~@; Filter.new :not, self, nil; end
114
117
 
115
118
 
@@ -119,10 +122,10 @@ class Filter
119
122
  "(!(#{@left}=#{@right}))"
120
123
  when :eq
121
124
  "(#{@left}=#{@right})"
122
- when :gt
123
- "#{@left}>#{@right}"
124
- when :lt
125
- "#{@left}<#{@right}"
125
+ #when :gt
126
+ # "#{@left}>#{@right}"
127
+ #when :lt
128
+ # "#{@left}<#{@right}"
126
129
  when :ge
127
130
  "#{@left}>=#{@right}"
128
131
  when :le
@@ -180,7 +183,7 @@ class Filter
180
183
  case @op
181
184
  when :eq
182
185
  if @right == "*" # present
183
- @left.to_ber_contextspecific 7
186
+ @left.to_s.to_ber_contextspecific 7
184
187
  elsif @right =~ /[\*]/ #substring
185
188
  ary = @right.split( /[\*]+/ )
186
189
  final_star = @right =~ /[\*]$/
@@ -191,17 +194,21 @@ class Filter
191
194
  seq << ary.shift.to_ber_contextspecific(0)
192
195
  end
193
196
  n_any_strings = ary.length - (final_star ? 0 : 1)
194
- p n_any_strings
197
+ #p n_any_strings
195
198
  n_any_strings.times {
196
199
  seq << ary.shift.to_ber_contextspecific(1)
197
200
  }
198
201
  unless final_star
199
202
  seq << ary.shift.to_ber_contextspecific(2)
200
203
  end
201
- [@left.to_ber, seq.to_ber].to_ber_contextspecific 4
204
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
202
205
  else #equality
203
- [@left.to_ber, @right.to_ber].to_ber_contextspecific 3
206
+ [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
204
207
  end
208
+ when :ge
209
+ [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
210
+ when :le
211
+ [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
205
212
  when :and
206
213
  ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
207
214
  ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
@@ -270,9 +277,106 @@ class Filter
270
277
  end
271
278
  end
272
279
 
280
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
281
+ # to a Net::LDAP::Filter.
282
+ def self.construct ldap_filter_string
283
+ FilterParser.new(ldap_filter_string).filter
284
+ end
285
+
286
+ # Synonym for #construct.
287
+ # to a Net::LDAP::Filter.
288
+ def self.from_rfc2254 ldap_filter_string
289
+ construct ldap_filter_string
290
+ end
273
291
 
274
292
  end # class Net::LDAP::Filter
275
293
 
294
+
295
+ class FilterParser #:nodoc:
296
+
297
+ attr_reader :filter
298
+
299
+ def initialize str
300
+ require 'strscan'
301
+ @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
302
+ end
303
+
304
+ def parse scanner
305
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
306
+ end
307
+
308
+ def parse_paren_expression scanner
309
+ if scanner.scan /\s*\(\s*/
310
+ b = if scanner.scan /\s*\&\s*/
311
+ a = nil
312
+ branches = []
313
+ while br = parse_paren_expression(scanner)
314
+ branches << br
315
+ end
316
+ if branches.length >= 2
317
+ a = branches.shift
318
+ while branches.length > 0
319
+ a = a & branches.shift
320
+ end
321
+ a
322
+ end
323
+ elsif scanner.scan /\s*\|\s*/
324
+ # TODO: DRY!
325
+ a = nil
326
+ branches = []
327
+ while br = parse_paren_expression(scanner)
328
+ branches << br
329
+ end
330
+ if branches.length >= 2
331
+ a = branches.shift
332
+ while branches.length > 0
333
+ a = a | branches.shift
334
+ end
335
+ a
336
+ end
337
+ elsif scanner.scan /\s*\!\s*/
338
+ br = parse_paren_expression(scanner)
339
+ if br
340
+ ~ br
341
+ end
342
+ else
343
+ parse_filter_branch( scanner )
344
+ end
345
+
346
+ if b and scanner.scan( /\s*\)\s*/ )
347
+ b
348
+ end
349
+ end
350
+ end
351
+
352
+ def parse_filter_branch scanner
353
+ scanner.scan /\s*/
354
+ if token = scanner.scan( /[\w\-_]+/ )
355
+ scanner.scan /\s*/
356
+ if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
357
+ scanner.scan /\s*/
358
+ if value = scanner.scan( /[\w\*\.]+/ )
359
+ case op
360
+ when "="
361
+ Filter.eq( token, value )
362
+ when "!="
363
+ Filter.ne( token, value )
364
+ when "<"
365
+ Filter.lt( token, value )
366
+ when "<="
367
+ Filter.le( token, value )
368
+ when ">"
369
+ Filter.gt( token, value )
370
+ when ">="
371
+ Filter.ge( token, value )
372
+ end
373
+ end
374
+ end
375
+ end
376
+ end
377
+
378
+ end # class Net::LDAP::FilterParser
379
+
276
380
  end # class Net::LDAP
277
381
  end # module Net
278
382
 
@@ -1,4 +1,4 @@
1
- # $Id: pdu.rb 85 2006-04-30 16:31:08Z blackhedd $
1
+ # $Id: pdu.rb 126 2006-05-31 15:55:16Z blackhedd $
2
2
  #
3
3
  # LDAP PDU support classes
4
4
  #
@@ -43,15 +43,20 @@ class LdapPdu
43
43
  AddResponse = 9
44
44
  DeleteResponse = 11
45
45
  ModifyRDNResponse = 13
46
+ SearchResultReferral = 19
46
47
 
47
48
  attr_reader :msg_id, :app_tag
48
49
  attr_reader :search_dn, :search_attributes, :search_entry
50
+ attr_reader :search_referrals
49
51
 
50
52
  #
51
53
  # initialize
52
54
  # An LDAP PDU always looks like a BerSequence with
53
- # two elements: an integer (message-id number), and
55
+ # at least two elements: an integer (message-id number), and
54
56
  # an application-specific sequence.
57
+ # Some LDAPv3 packets also include an optional
58
+ # third element, which is a sequence of "controls"
59
+ # (See RFC 2251, section 4.1.12).
55
60
  # The application-specific tag in the sequence tells
56
61
  # us what kind of packet it is, and each kind has its
57
62
  # own format, defined in RFC-1777.
@@ -62,6 +67,10 @@ class LdapPdu
62
67
  # it remains to be seen whether there are servers out
63
68
  # there that will not work well with our approach.
64
69
  #
70
+ # Added a controls-processor to SearchResult.
71
+ # Didn't add it everywhere because it just _feels_
72
+ # like it will need to be refactored.
73
+ #
65
74
  def initialize ber_object
66
75
  begin
67
76
  @msg_id = ber_object[0].to_i
@@ -76,8 +85,11 @@ class LdapPdu
76
85
  parse_ldap_result ber_object[1]
77
86
  when SearchReturnedData
78
87
  parse_search_return ber_object[1]
88
+ when SearchResultReferral
89
+ parse_search_referral ber_object[1]
79
90
  when SearchResult
80
91
  parse_ldap_result ber_object[1]
92
+ parse_controls(ber_object[2]) if ber_object[2]
81
93
  when ModifyResponse
82
94
  parse_ldap_result ber_object[1]
83
95
  when AddResponse
@@ -101,8 +113,12 @@ class LdapPdu
101
113
  @ldap_result and @ldap_result[code]
102
114
  end
103
115
 
116
+ # Return RFC-2251 Controls if any.
117
+ # Messy. Does this functionality belong somewhere else?
118
+ def result_controls
119
+ @ldap_controls || []
120
+ end
104
121
 
105
- private
106
122
 
107
123
  #
108
124
  # parse_ldap_result
@@ -111,6 +127,7 @@ class LdapPdu
111
127
  sequence.length >= 3 or raise LdapPduError
112
128
  @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]}
113
129
  end
130
+ private :parse_ldap_result
114
131
 
115
132
  #
116
133
  # parse_search_return
@@ -136,17 +153,50 @@ class LdapPdu
136
153
  # we also return @search_entry, which is an LDAP::Entry object.
137
154
  # If that works out well, then we'll remove the first two.
138
155
  #
156
+ # Provisionally removed obsolete search_attributes and search_dn, 04May06.
157
+ #
139
158
  def parse_search_return sequence
140
159
  sequence.length >= 2 or raise LdapPduError
141
160
  @search_entry = LDAP::Entry.new( sequence[0] )
142
- @search_dn = sequence[0]
143
- @search_attributes = {}
161
+ #@search_dn = sequence[0]
162
+ #@search_attributes = {}
144
163
  sequence[1].each {|seq|
145
164
  @search_entry[seq[0]] = seq[1]
146
- @search_attributes[seq[0].downcase.intern] = seq[1]
165
+ #@search_attributes[seq[0].downcase.intern] = seq[1]
147
166
  }
148
167
  end
149
168
 
169
+ #
170
+ # A search referral is a sequence of one or more LDAP URIs.
171
+ # Any number of search-referral replies can be returned by the server, interspersed
172
+ # with normal replies in any order.
173
+ # Until I can think of a better way to do this, we'll return the referrals as an array.
174
+ # It'll be up to higher-level handlers to expose something reasonable to the client.
175
+ def parse_search_referral uris
176
+ @search_referrals = uris
177
+ end
178
+
179
+
180
+ # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting
181
+ # of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL
182
+ # Octet String. If only two fields are given, the second one may be
183
+ # either criticality or data, since criticality has a default value.
184
+ # Someday we may want to come back here and add support for some of
185
+ # more-widely used controls. RFC-2696 is a good example.
186
+ #
187
+ def parse_controls sequence
188
+ @ldap_controls = sequence.map do |control|
189
+ o = OpenStruct.new
190
+ o.oid,o.criticality,o.value = control[0],control[1],control[2]
191
+ if o.criticality and o.criticality.is_a?(String)
192
+ o.value = o.criticality
193
+ o.criticality = false
194
+ end
195
+ o
196
+ end
197
+ end
198
+ private :parse_controls
199
+
150
200
 
151
201
  end
152
202
 
@@ -1,4 +1,4 @@
1
- # $Id: testem.rb 72 2006-04-24 21:58:14Z blackhedd $
1
+ # $Id: testem.rb 121 2006-05-15 18:36:24Z blackhedd $
2
2
  #
3
3
  #
4
4
 
@@ -7,5 +7,6 @@ require 'tests/testber'
7
7
  require 'tests/testldif'
8
8
  require 'tests/testldap'
9
9
  require 'tests/testpsw'
10
+ require 'tests/testfilter'
10
11
 
11
12
 
@@ -0,0 +1,37 @@
1
+ # $Id: testfilter.rb 122 2006-05-15 20:03:56Z blackhedd $
2
+ #
3
+ #
4
+
5
+ require 'test/unit'
6
+
7
+ $:.unshift "lib"
8
+
9
+ require 'net/ldap'
10
+
11
+
12
+ class TestFilter < Test::Unit::TestCase
13
+
14
+ def setup
15
+ end
16
+
17
+
18
+ def teardown
19
+ end
20
+
21
+ def test_rfc_2254
22
+ p Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " )
23
+ p Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
24
+ p Net::LDAP::Filter.from_rfc2254( "uid<george*" )
25
+ p Net::LDAP::Filter.from_rfc2254( "uid <= george*" )
26
+ p Net::LDAP::Filter.from_rfc2254( "uid>george*" )
27
+ p Net::LDAP::Filter.from_rfc2254( "uid>=george*" )
28
+ p Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
29
+
30
+ p Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" )
31
+ p Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" )
32
+ p Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" )
33
+ end
34
+
35
+
36
+ end
37
+
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: ruby-net-ldap
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.1
7
- date: 2006-05-01 00:00:00 -04:00
6
+ version: 0.0.2
7
+ date: 2006-07-12 00:00:00 -04:00
8
8
  summary: A pure Ruby LDAP client library.
9
9
  require_paths:
10
10
  - lib
@@ -32,6 +32,7 @@ files:
32
32
  - LICENCE
33
33
  - ChangeLog
34
34
  - COPYING
35
+ - tests/testfilter.rb
35
36
  - tests/testpsw.rb
36
37
  - tests/testem.rb
37
38
  - tests/testdata.ldif