ruby-net-ldap 0.0.1 → 0.0.2

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